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 exposes the process-wide DeltaT model: read the current function, install a custom one, or restore the built-in default.
// Package astro 提供进程级时标配置与时标换算 / process-wide time-scale configuration and conversions.
package astro
import "b612.me/astro/basic"
// DeltaT 返回当前的 TT−UT1(ΔT)模型 / the active TT−UT1 (DeltaT) model.
func DeltaT() func(float64, bool) float64 {
return basic.GetDeltaTFn()
}
// SetDeltaT 替换 ΔT 模型(入参为数值与“该数值是否为儒略日”,为假时是十进制年),传 nil 恢复默认 / replaces the DeltaT model; nil restores the default.
func SetDeltaT(deltaT func(float64, bool) float64) {
basic.SetDeltaTFn(deltaT)
}
// DefaultDeltaT 返回内置默认 ΔT 模型 / the built-in default DeltaT model.
func DefaultDeltaT() func(float64, bool) float64 {
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
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 {
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) {
@@ -23,7 +23,7 @@ func assertAncientNextStation(t *testing.T, queryTT, gotUT, wantUT float64) {
}
if diff := math.Abs(gotUT - wantUT); diff > ancientStationTolerance {
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.
type ApsisEvent struct {
// JDE 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JDE float64
// JD 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JD float64
// Distance 是极值距离;地球相关事件单位 AU,月球相关事件单位 km / extremum distance.
Distance float64
}
@@ -79,7 +79,7 @@ func earthApsis(year int, aphelion bool) ApsisEvent {
}
eventTT, distanceAU := refineDistanceExtremum(seedTT, cfg, EarthAway)
return ApsisEvent{
JDE: TD2UT(eventTT, false),
JD: TT2UTC(eventTT),
Distance: distanceAU,
}
}
@@ -87,8 +87,8 @@ func earthApsis(year int, aphelion bool) ApsisEvent {
func moonApsisInMonth(year int, month time.Month, apogee bool) []ApsisEvent {
startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
endUTC := startUTC.AddDate(0, 1, 0)
startTT := TD2UT(Date2JDE(startUTC), true)
endTT := TD2UT(Date2JDE(endUTC), true)
startTT := UTC2TT(Date2JD(startUTC))
endTT := UTC2TT(Date2JD(endUTC))
kStart := int(math.Floor((startTT-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++ {
seedTT := moonApsisSeedTT(float64(k) + phase)
eventTT, distanceKM := refineDistanceExtremum(seedTT, cfg, HMoonAway)
eventUT := TD2UT(eventTT, false)
eventTimeUTC := JDE2DateByZone(eventUT, time.UTC, false)
eventUT := TT2UTC(eventTT)
eventTimeUTC := JD2DateByZone(eventUT, time.UTC, false)
if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) {
continue
}
events = append(events, ApsisEvent{
JDE: eventUT,
JD: eventUT,
Distance: distanceKM,
})
}
sort.Slice(events, func(i, j int) bool {
return events[i].JDE < events[j].JDE
return events[i].JD < events[j].JD
})
return events
}
+2 -2
View File
@@ -62,7 +62,7 @@ func TestEarthApsisMatchesHorizonsBaseline(t *testing.T) {
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)
if timeDiff < 0 {
timeDiff = -timeDiff
@@ -139,7 +139,7 @@ func TestMoonApsisMatchesHorizonsBaseline(t *testing.T) {
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)
if timeDiff < 0 {
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
}
jd := JDECalc(year, int(month), float64(day))
jd := JDCalc(year, int(month), float64(day))
if angle == 0 {
angle = 360
}
@@ -43,7 +43,7 @@ func TestGetJQTime(t *testing.T) {
jd -= 0.001
}
jd += 0.001
return TD2UT(jd, false)
return TT2UTC(jd)
}
testCases := []struct {
+1 -1
View File
@@ -65,7 +65,7 @@ func TestConstellationNameLookups(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" {
t.Fatalf("ConstellationNameEN() = %q, want %q", en, "Orion")
}
+3 -3
View File
@@ -5,7 +5,7 @@ import (
)
func TestConstellationNameZH(t *testing.T) {
now := GetNowJDE()
now := GetNowJD()
//finish on 30s
for i := 0.00; i <= 360.00; i += 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) {
jde := GetNowJDE()
jde := GetNowJD()
for i := 0; i < b.N; i++ {
//GetNowJDE()
//GetNowJD()
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) {
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) {
@@ -109,43 +109,41 @@ func TopocentricDec(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格
return topocentricDec
}
func TopocentricLo(lo, bo, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
c := pcosi(lat, h)
s := psini(lat, 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
// TopocentricLoBo 一次求值给出站心黄经与黄纬 / topocentric ecliptic longitude and latitude in one solve.
//
// 先解站心赤道坐标再转黄道:黄道版公式的分母在黄经 90°–270° 时变号,直接 atan2 会切到对顶象限。
func TopocentricLoBo(lo, bo, lat, lon, jde, au, h float64) (float64, float64) {
ra, dec := LoBoToRaDec(jde, lo, bo)
topRA, topDec := TopocentricRaDec(ra, dec, lat, lon, jde, au, h)
return RaDecToLoBo(jde, topRA, topDec)
}
// 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
}
func TopocentricBo(lo, bo, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
c := pcosi(lat, h)
s := psini(lat, 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
// TopocentricBo 站心黄纬 / topocentric ecliptic latitude.
func TopocentricBo(lo, bo, lat, lon, jde, au, h float64) float64 {
_, nbo := TopocentricLoBo(lo, bo, lat, lon, jde, au, h)
return nbo
}
func GXCLo(lo, bo, jd float64) float64 { //光行差修正
func GXCLo(lo, bo, jde float64) float64 { //光行差修正
k := 20.49552
sunlo := SunTrueLo(jd)
e := Earthe(jd)
epi := EarthPI(jd)
sunlo := SunTrueLo(jde)
e := Earthe(jde)
epi := EarthPI(jde)
tmp := (-k*Cos(sunlo-lo) + e*k*Cos(epi-lo)) / Cos(bo)
return tmp
}
func GXCBo(lo, bo, jd float64) float64 {
func GXCBo(lo, bo, jde float64) float64 {
k := 20.49552
sunlo := SunTrueLo(jd)
e := Earthe(jd)
epi := EarthPI(jd)
sunlo := SunTrueLo(jde)
e := Earthe(jde)
epi := EarthPI(jde)
tmp := -k * Sin(bo) * (Sin(sunlo-lo) - e*Sin(epi-lo))
return tmp
}
+10 -9
View File
@@ -9,27 +9,28 @@ import (
)
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
dec := -5.973400893
lat := 6.79657
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)
wantRA, wantDec := independentTopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0)
if delta := angularDistanceArcsec(gotRA, gotDec, wantRA, wantDec); delta > 1e-6 {
t.Fatalf("TopocentricRaDec differs from independent formula by %.9f arcsec", delta)
// 逐分量比较:零距离处 acos 度规的病态下限会把 1 ulp 放大成毫角秒。
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) {
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
dec := -5.973400893
lat := 6.79657
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)
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) {
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
latitude := 51.4779
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))
got := HMoonHeightN(ut, longitude, latitude, 0, -1)
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)
rhoSin := polarRadiusKM/equatorialRadiusKM*math.Sin(u) + height/6378140.0*Sin(lat)
sinParallax := Sin(0.0024427777777) / distanceAU
hourAngle := Limit360(ApparentSiderealTime(ut)*15 + lon - ra)
hourAngle := Limit360(ApparentSiderealTime(UTC2UT1(ut))*15 + lon - ra)
deltaRA := math.Atan2(
-rhoCos*sinParallax*Sin(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.MoonCulminationTime 的返回值就是本地民用时框架,moon/moon.go 按 byZone=true 使用。
@@ -33,7 +33,7 @@ var culminationAnchorSites = []culminationAnchorSite{
func culminationAnchorLocalMidnight(site culminationAnchorSite, timestamp time.Time) (time.Time, float64) {
location := time.FixedZone("anchor", int(site.tz*3600))
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) {
@@ -42,7 +42,7 @@ func TestSunCulminationAnchorKeepsLocalDay(t *testing.T) {
local, midnightJD := culminationAnchorLocalMidnight(site, timestamp)
location := local.Location()
// 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
for minute := 0; minute <= 24*60; minute++ {
jd := midnightJD + float64(minute)/1440.0
@@ -50,7 +50,7 @@ func TestSunCulminationAnchorKeepsLocalDay(t *testing.T) {
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() {
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 {
local, midnightJD := culminationAnchorLocalMidnight(site, timestamp)
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
for minute := 0; minute <= 24*60; minute++ {
jd := midnightJD + float64(minute)/1440.0
@@ -73,7 +73,7 @@ func TestMoonCulminationAnchorKeepsLocalDay(t *testing.T) {
truthAltitude, truthJD = altitude, jd
}
}
truth := JDE2DateByZone(truthJD, location, true)
truth := JD2DateByZone(truthJD, location, true)
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"))
}
-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) {
for _, year := range []float64{-700.5, -0.5, 0, 1000.5, 1582.75, 1800.5, 2000.5, 2026.5} {
whole := math.Floor(year)
start := JDECalc(int(whole), 1, 1)
end := JDECalc(int(whole)+1, 1, 1)
start := JDCalc(int(whole), 1, 1)
end := JDCalc(int(whole)+1, 1, 1)
want := DefaultDeltaTv2(start+(year-whole)*(end-start), true)
got := DefaultDeltaTv2(year, false)
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
if got := DeltaTv2(boundary); got != 69.184 {
t.Fatalf("DeltaT at leap-second boundary=%v, want 69.184", got)
before := DeltaTv2(boundary - 1e-6)
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) {
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} {
ut := JDECalc(year, 9, 20.123456)
tt := TD2UT(ut, true)
if got := TD2UT(tt, false); math.Abs(got-ut) > math.Nextafter(ut, math.Inf(1))-ut {
t.Errorf("year=%d UT round trip differs by %.9f seconds", year, (got-ut)*86400)
utc := JDCalc(year, 9, 20.123456)
tt := UTC2TT(utc)
if got := TT2UTC(tt); math.Abs(got-utc) > math.Nextafter(utc, math.Inf(1))-utc {
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)
}
}
for _, seconds := range []float64{-70, -1, -0.1, 0, 0.1, 1, 70} {
ut := 2457754.5 + seconds/86400
if got := TD2UT(TD2UT(ut, true), false); got != ut {
t.Errorf("leap second offset=%g round trip differs by %.9f seconds", seconds, (got-ut)*86400)
utc := 2457754.5 + seconds/86400
if got := TT2UTC(UTC2TT(utc)); got != utc {
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()
t.Cleanup(func() { SetDeltaTFn(original) })
for _, slope := range []float64{0, 0.01} {
@@ -67,9 +73,9 @@ func TestTD2UTCustomDeltaT(t *testing.T) {
}
return 10000 + slope*(jd-2451545)
})
ut := 3000000.123456
if got := TD2UT(TD2UT(ut, true), false); got != ut {
t.Errorf("custom slope=%g round trip differs by %.9f seconds", slope, (got-ut)*86400)
utc := 3000000.123456
if got := TT2UTC(UTC2TT(utc)); got != utc {
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.
func SunSemidiameter(jd float64) float64 {
return SunSemidiameterN(jd, -1)
func SunSemidiameter(jde float64) float64 {
return SunSemidiameterN(jde, -1)
}
// SunSemidiameterN 太阳视半径(截断版),单位角秒 / truncated apparent solar semidiameter in arcseconds.
func SunSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(sunEquatorialRadiusKM, EarthAwayN(jd, n))
func SunSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(sunEquatorialRadiusKM, EarthAwayN(jde, n))
}
// SunDiameter 太阳视直径,单位角秒 / apparent solar diameter in arcseconds.
func SunDiameter(jd float64) float64 {
return SunDiameterN(jd, -1)
func SunDiameter(jde float64) float64 {
return SunDiameterN(jde, -1)
}
// SunDiameterN 太阳视直径(截断版),单位角秒 / truncated apparent solar diameter in arcseconds.
func SunDiameterN(jd float64, n int) float64 {
return 2 * SunSemidiameterN(jd, n)
func SunDiameterN(jde float64, n int) float64 {
return 2 * SunSemidiameterN(jde, n)
}
// MoonSemidiameter 月亮视半径,单位角秒 / apparent lunar semidiameter in arcseconds.
func MoonSemidiameter(jd float64) float64 {
return MoonSemidiameterN(jd, -1)
func MoonSemidiameter(jde float64) float64 {
return MoonSemidiameterN(jde, -1)
}
// MoonSemidiameterN 月亮视半径(截断版),单位角秒 / truncated apparent lunar semidiameter in arcseconds.
func MoonSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterArcsec(moonEquatorialRadiusKM, HMoonAwayN(jd, n))
func MoonSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterArcsec(moonEquatorialRadiusKM, HMoonAwayN(jde, n))
}
// MoonDiameter 月亮视直径,单位角秒 / apparent lunar diameter in arcseconds.
func MoonDiameter(jd float64) float64 {
return MoonDiameterN(jd, -1)
func MoonDiameter(jde float64) float64 {
return MoonDiameterN(jde, -1)
}
// MoonDiameterN 月亮视直径(截断版),单位角秒 / truncated apparent lunar diameter in arcseconds.
func MoonDiameterN(jd float64, n int) float64 {
return 2 * MoonSemidiameterN(jd, n)
func MoonDiameterN(jde float64, n int) float64 {
return 2 * MoonSemidiameterN(jde, n)
}
// MercurySemidiameter 水星视半径,单位角秒 / apparent Mercury semidiameter in arcseconds.
func MercurySemidiameter(jd float64) float64 {
return MercurySemidiameterN(jd, -1)
func MercurySemidiameter(jde float64) float64 {
return MercurySemidiameterN(jde, -1)
}
// MercurySemidiameterN 水星视半径(截断版),单位角秒 / truncated apparent Mercury semidiameter in arcseconds.
func MercurySemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(mercuryEquatorialRadiusKM, EarthMercuryAwayN(jd, n))
func MercurySemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(mercuryEquatorialRadiusKM, EarthMercuryAwayN(jde, n))
}
// MercuryDiameter 水星视直径,单位角秒 / apparent Mercury diameter in arcseconds.
func MercuryDiameter(jd float64) float64 {
return MercuryDiameterN(jd, -1)
func MercuryDiameter(jde float64) float64 {
return MercuryDiameterN(jde, -1)
}
// MercuryDiameterN 水星视直径(截断版),单位角秒 / truncated apparent Mercury diameter in arcseconds.
func MercuryDiameterN(jd float64, n int) float64 {
return 2 * MercurySemidiameterN(jd, n)
func MercuryDiameterN(jde float64, n int) float64 {
return 2 * MercurySemidiameterN(jde, n)
}
// VenusSemidiameter 金星视半径,单位角秒 / apparent Venus semidiameter in arcseconds.
func VenusSemidiameter(jd float64) float64 {
return VenusSemidiameterN(jd, -1)
func VenusSemidiameter(jde float64) float64 {
return VenusSemidiameterN(jde, -1)
}
// VenusSemidiameterN 金星视半径(截断版),单位角秒 / truncated apparent Venus semidiameter in arcseconds.
func VenusSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(venusEquatorialRadiusKM, EarthVenusAwayN(jd, n))
func VenusSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(venusEquatorialRadiusKM, EarthVenusAwayN(jde, n))
}
// VenusDiameter 金星视直径,单位角秒 / apparent Venus diameter in arcseconds.
func VenusDiameter(jd float64) float64 {
return VenusDiameterN(jd, -1)
func VenusDiameter(jde float64) float64 {
return VenusDiameterN(jde, -1)
}
// VenusDiameterN 金星视直径(截断版),单位角秒 / truncated apparent Venus diameter in arcseconds.
func VenusDiameterN(jd float64, n int) float64 {
return 2 * VenusSemidiameterN(jd, n)
func VenusDiameterN(jde float64, n int) float64 {
return 2 * VenusSemidiameterN(jde, n)
}
// MarsSemidiameter 火星视半径,单位角秒 / apparent Mars semidiameter in arcseconds.
func MarsSemidiameter(jd float64) float64 {
return MarsSemidiameterN(jd, -1)
func MarsSemidiameter(jde float64) float64 {
return MarsSemidiameterN(jde, -1)
}
// MarsSemidiameterN 火星视半径(截断版),单位角秒 / truncated apparent Mars semidiameter in arcseconds.
func MarsSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(marsEquatorialRadiusKM, EarthMarsAwayN(jd, n))
func MarsSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(marsEquatorialRadiusKM, EarthMarsAwayN(jde, n))
}
// MarsDiameter 火星视直径,单位角秒 / apparent Mars diameter in arcseconds.
func MarsDiameter(jd float64) float64 {
return MarsDiameterN(jd, -1)
func MarsDiameter(jde float64) float64 {
return MarsDiameterN(jde, -1)
}
// MarsDiameterN 火星视直径(截断版),单位角秒 / truncated apparent Mars diameter in arcseconds.
func MarsDiameterN(jd float64, n int) float64 {
return 2 * MarsSemidiameterN(jd, n)
func MarsDiameterN(jde float64, n int) float64 {
return 2 * MarsSemidiameterN(jde, n)
}
// JupiterSemidiameter 木星视半径,单位角秒 / apparent Jupiter semidiameter in arcseconds.
func JupiterSemidiameter(jd float64) float64 {
return JupiterSemidiameterN(jd, -1)
func JupiterSemidiameter(jde float64) float64 {
return JupiterSemidiameterN(jde, -1)
}
// JupiterSemidiameterN 木星视半径(截断版),单位角秒 / truncated apparent Jupiter semidiameter in arcseconds.
func JupiterSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(jupiterEquatorialRadiusKM, EarthJupiterAwayN(jd, n))
func JupiterSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(jupiterEquatorialRadiusKM, EarthJupiterAwayN(jde, n))
}
// JupiterDiameter 木星视直径,单位角秒 / apparent Jupiter diameter in arcseconds.
func JupiterDiameter(jd float64) float64 {
return JupiterDiameterN(jd, -1)
func JupiterDiameter(jde float64) float64 {
return JupiterDiameterN(jde, -1)
}
// JupiterDiameterN 木星视直径(截断版),单位角秒 / truncated apparent Jupiter diameter in arcseconds.
func JupiterDiameterN(jd float64, n int) float64 {
return 2 * JupiterSemidiameterN(jd, n)
func JupiterDiameterN(jde float64, n int) float64 {
return 2 * JupiterSemidiameterN(jde, n)
}
// SaturnSemidiameter 土星视半径,单位角秒 / apparent Saturn semidiameter in arcseconds.
func SaturnSemidiameter(jd float64) float64 {
return SaturnSemidiameterN(jd, -1)
func SaturnSemidiameter(jde float64) float64 {
return SaturnSemidiameterN(jde, -1)
}
// SaturnSemidiameterN 土星视半径(截断版),单位角秒 / truncated apparent Saturn semidiameter in arcseconds.
func SaturnSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(saturnEquatorialRadiusKM, EarthSaturnAwayN(jd, n))
func SaturnSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(saturnEquatorialRadiusKM, EarthSaturnAwayN(jde, n))
}
// SaturnDiameter 土星视直径,单位角秒 / apparent Saturn diameter in arcseconds.
func SaturnDiameter(jd float64) float64 {
return SaturnDiameterN(jd, -1)
func SaturnDiameter(jde float64) float64 {
return SaturnDiameterN(jde, -1)
}
// SaturnDiameterN 土星视直径(截断版),单位角秒 / truncated apparent Saturn diameter in arcseconds.
func SaturnDiameterN(jd float64, n int) float64 {
return 2 * SaturnSemidiameterN(jd, n)
func SaturnDiameterN(jde float64, n int) float64 {
return 2 * SaturnSemidiameterN(jde, n)
}
// UranusSemidiameter 天王星视半径,单位角秒 / apparent Uranus semidiameter in arcseconds.
func UranusSemidiameter(jd float64) float64 {
return UranusSemidiameterN(jd, -1)
func UranusSemidiameter(jde float64) float64 {
return UranusSemidiameterN(jde, -1)
}
// UranusSemidiameterN 天王星视半径(截断版),单位角秒 / truncated apparent Uranus semidiameter in arcseconds.
func UranusSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(uranusEquatorialRadiusKM, EarthUranusAwayN(jd, n))
func UranusSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(uranusEquatorialRadiusKM, EarthUranusAwayN(jde, n))
}
// UranusDiameter 天王星视直径,单位角秒 / apparent Uranus diameter in arcseconds.
func UranusDiameter(jd float64) float64 {
return UranusDiameterN(jd, -1)
func UranusDiameter(jde float64) float64 {
return UranusDiameterN(jde, -1)
}
// UranusDiameterN 天王星视直径(截断版),单位角秒 / truncated apparent Uranus diameter in arcseconds.
func UranusDiameterN(jd float64, n int) float64 {
return 2 * UranusSemidiameterN(jd, n)
func UranusDiameterN(jde float64, n int) float64 {
return 2 * UranusSemidiameterN(jde, n)
}
// NeptuneSemidiameter 海王星视半径,单位角秒 / apparent Neptune semidiameter in arcseconds.
func NeptuneSemidiameter(jd float64) float64 {
return NeptuneSemidiameterN(jd, -1)
func NeptuneSemidiameter(jde float64) float64 {
return NeptuneSemidiameterN(jde, -1)
}
// NeptuneSemidiameterN 海王星视半径(截断版),单位角秒 / truncated apparent Neptune semidiameter in arcseconds.
func NeptuneSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(neptuneEquatorialRadiusKM, EarthNeptuneAwayN(jd, n))
func NeptuneSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(neptuneEquatorialRadiusKM, EarthNeptuneAwayN(jde, n))
}
// NeptuneDiameter 海王星视直径,单位角秒 / apparent Neptune diameter in arcseconds.
func NeptuneDiameter(jd float64) float64 {
return NeptuneDiameterN(jd, -1)
func NeptuneDiameter(jde float64) float64 {
return NeptuneDiameterN(jde, -1)
}
// NeptuneDiameterN 海王星视直径(截断版),单位角秒 / truncated apparent Neptune diameter in arcseconds.
func NeptuneDiameterN(jd float64, n int) float64 {
return 2 * NeptuneSemidiameterN(jd, n)
func NeptuneDiameterN(jde float64, n int) float64 {
return 2 * NeptuneSemidiameterN(jde, n)
}
+2 -2
View File
@@ -51,7 +51,7 @@ func TestAngularDiametersMatchHorizonsBaseline(t *testing.T) {
if err != nil {
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 {
want := sample.Values[tc.baselineKey]
got := tc.diameter(jd)
@@ -75,7 +75,7 @@ func TestAngularDiametersMatchHorizonsBaseline(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 {
name string
+2 -2
View File
@@ -7,7 +7,7 @@ import (
)
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 {
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) {
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,
32.7767,
0,
+2 -2
View File
@@ -94,12 +94,12 @@ func greatestTimeContourAlignedLevels(startTT, endTT float64, step time.Duration
// greatestTimeContourTTToUTC 把 TT 儒略日换成对应的 UTC 时刻。
func greatestTimeContourTTToUTC(tt float64) time.Time {
return JDE2DateByZone(TD2UT(tt, false), time.UTC, false)
return JD2DateByZone(TT2UTC(tt), time.UTC, false)
}
// greatestTimeContourUTCToTT 把 UTC 时刻换成对应的 TT 儒略日。
func greatestTimeContourUTCToTT(value time.Time) float64 {
return TD2UT(Date2JDE(value.UTC()), true)
return UTC2TT(Date2JD(value.UTC()))
}
// greatestTimeContourPointSegmentKM 返回点到折线段的距离,单位 km。
+1 -1
View File
@@ -8,7 +8,7 @@ import (
// 对齐网格是导出行为(等时线按整刻度取值)的契约,取值本身写成字面量以免与实际生成函数互相自证。
func TestGreatestTimeContourAlignedLevelsGridContracts(t *testing.T) {
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 {
name string
+3 -3
View File
@@ -9,7 +9,7 @@ const (
)
func eventQueryTTAsUT(queryTT float64) float64 {
return TD2UT(queryTT, false)
return TT2UTC(queryTT)
}
func eventUTQueryTTDelta(eventUT, queryTT float64) float64 {
@@ -25,11 +25,11 @@ func eventUTQueryAfterOrEqual(eventUT, queryTT float64) bool {
}
func eventUTNextQueryTT(eventUT float64) float64 {
return TD2UT(eventUT, true) + 1.0
return UTC2TT(eventUT) + 1.0
}
func eventUTLastQueryTT(eventUT float64) float64 {
return TD2UT(eventUT, true) - 1.0
return UTC2TT(eventUT) - 1.0
}
func innerNextCycleOffset(delta, period float64) float64 {
+3 -3
View File
@@ -34,7 +34,7 @@ func TestInnerPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
queryTT := TD2UT(tc.seed, true)
queryTT := UTC2TT(tc.seed)
last := tc.lastFn(queryTT)
next := tc.nextFn(queryTT)
if !sameEventJD(last, tc.seed) {
@@ -64,7 +64,7 @@ func TestInnerPlanetNextEventAdvancesPastReturnedEvent(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
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)
if !eventUTQueryAfterOrEqual(next, query) {
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 {
t.Run(tc.name, func(t *testing.T) {
queryTT := TD2UT(tc.seed, true)
queryTT := UTC2TT(tc.seed)
last := tc.last(queryTT)
next := tc.next(queryTT)
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)
before := when.Add(-24 * time.Hour)
after := when.Add(24 * time.Hour)
next := JDE2DateByZone(nextFn(toUTJD(before)), when.Location(), false)
last := JDE2DateByZone(lastFn(toUTJD(after)), when.Location(), false)
next := JD2DateByZone(nextFn(toUTJD(before)), when.Location(), false)
last := JD2DateByZone(lastFn(toUTJD(after)), when.Location(), false)
tolerance := innerBaselineTolerance(event)
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 timeNow = time.Now
// Date2JDE 日期转儒略日
func Date2JDE(date time.Time) float64 {
// Date2JD 日期转儒略日
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
return JDECalc(date.Year(), int(date.Month()), day)
return JDCalc(date.Year(), int(date.Month()), day)
}
func ValidateCivilDate(year, month int, day float64) error {
@@ -68,12 +68,10 @@ func isCivilLeapYear(year, month int, day float64) bool {
return year%4 == 0
}
/*
@name: 儒略日计算
@dec: 计算给定时间的儒略日,1582年改力后为格里高利历,之前为儒略历
@ 请注意,传入的时间在天文计算中一般为力学时,应当注意和世界时的转化
*/
func JDECalc(year, month int, day float64) float64 {
// JDCalc 由公历年月日(day 可含小数)计算儒略日 / Julian day from a civil year, month and fractional day.
// 1582 年 10 月 15 日起按格里高利历,之前按儒略历;日期非法时返回 NaN。
// Gregorian from 1582-10-15 onward and Julian before it; an invalid civil date yields NaN.
func JDCalc(year, month int, day float64) float64 {
if err := ValidateCivilDate(year, month, day); err != nil {
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)
}
/*
@name: 获得当前儒略日时间:当地世界时,非格林尼治时间
*/
func GetNowJDE() (nowJDE float64) {
// GetNowJD 按当前时区的日历字段取儒略日 / Julian day from the current clock's calendar fields.
// 读的是当前时区的年月日时分秒,不做时区归算。
// It reads the calendar fields in the current location without any zone conversion.
func GetNowJD() (nowJD float64) {
now := timeNow()
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
}
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
z := float64(int(jd))
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))
}
// JDE2DateByZone JDE(儒略日)转日期
// JD2DateByZone 儒略日转日期
// jd: 儒略日
// tz: 目标时区
// 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
z := float64(int(jd))
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
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 {
transTz = time.UTC
return date.In(tz)
}
return time.Date(int(years), time.Month(int(months)), int(days), 0, 0, 0, 0, transTz).
Add(time.Duration(int64(1000000000 * tms))).In(tz)
// 当地 JD 的小数部分是钟表读数,不能按夏令时午夜后的实际时长累加。
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"
)
func TestGetNowJDEUsesSingleTimestamp(t *testing.T) {
func TestGetNowJDUsesSingleTimestamp(t *testing.T) {
oldTimeNow := timeNow
defer func() {
timeNow = oldTimeNow
@@ -23,12 +23,12 @@ func TestGetNowJDEUsesSingleTimestamp(t *testing.T) {
return second
}
got := GetNowJDE()
want := Date2JDE(first)
got := GetNowJD()
want := Date2JD(first)
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) {
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"
)
func JupiterL(jd float64) float64 {
return planet.WherePlanet(4, 0, jd)
func JupiterL(jde float64) float64 {
return planet.WherePlanet(4, 0, jde)
}
func JupiterB(jd float64) float64 {
return planet.WherePlanet(4, 1, jd)
func JupiterB(jde float64) float64 {
return planet.WherePlanet(4, 1, jde)
}
func JupiterR(jd float64) float64 {
return planet.WherePlanet(4, 2, jd)
func JupiterR(jde float64) float64 {
return planet.WherePlanet(4, 2, jde)
}
func AJupiterX(jd float64) float64 {
l := JupiterL(jd)
b := JupiterB(jd)
r := JupiterR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func AJupiterX(jde float64) float64 {
l := JupiterL(jde)
b := JupiterB(jde)
r := JupiterR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x
}
func AJupiterY(jd float64) float64 {
func AJupiterY(jde float64) float64 {
l := JupiterL(jd)
b := JupiterB(jd)
r := JupiterR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
l := JupiterL(jde)
b := JupiterB(jde)
r := JupiterR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y
}
func AJupiterZ(jd float64) float64 {
//l := JupiterL(jd)
b := JupiterB(jd)
r := JupiterR(jd)
// el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func AJupiterZ(jde float64) float64 {
//l := JupiterL(jde)
b := JupiterB(jde)
r := JupiterR(jde)
// el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
z := r*Sin(b) - er*Sin(eb)
return z
}
func AJupiterXYZ(jd float64) (float64, float64, float64) {
l := JupiterL(jd)
b := JupiterB(jd)
r := JupiterR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func AJupiterXYZ(jde float64) (float64, float64, float64) {
l := JupiterL(jde)
b := JupiterB(jde)
r := JupiterR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb)
return x, y, z
}
func JupiterApparentRa(jd float64) float64 {
lo, bo := JupiterApparentLoBo(jd)
eps := TrueObliquity(jd)
func JupiterApparentRa(jde float64) float64 {
lo, bo := JupiterApparentLoBo(jde)
eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
return Limit360(ra)
}
func JupiterApparentDec(jd float64) float64 {
lo, bo := JupiterApparentLoBo(jd)
eps := TrueObliquity(jd)
func JupiterApparentDec(jde float64) float64 {
lo, bo := JupiterApparentLoBo(jde)
eps := TrueObliquity(jde)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec
}
func JupiterApparentRaDec(jd float64) (float64, float64) {
lo, bo := JupiterApparentLoBo(jd)
eps := TrueObliquity(jd)
func JupiterApparentRaDec(jde float64) (float64, float64) {
lo, bo := JupiterApparentLoBo(jde)
eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
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
}
func JupiterMag(jd float64) float64 {
sunDistance := JupiterR(jd)
earthDistance := EarthJupiterAway(jd)
earthSunDistance := planet.WherePlanet(-1, 2, jd)
func JupiterMag(jde float64) float64 {
sunDistance := JupiterR(jde)
earthDistance := EarthJupiterAway(jde)
earthSunDistance := planet.WherePlanet(-1, 2, jde)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(i)
mag := -9.40 + 5*math.Log10(sunDistance*earthDistance) + 0.0005*i
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))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := JupiterApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
@@ -129,12 +129,12 @@ func JupiterHeight(jde, lon, lat, timezone float64) float64 {
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))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := JupiterApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 三角转换公式
@@ -153,21 +153,21 @@ func JupiterAzimuth(jde, lon, lat, timezone float64) float64 {
}
func JupiterHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon)
hourAngle := siderealLongitude - JupiterApparentRa(TD2UT(jd-timezone/24.0, true))
siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - JupiterApparentRa(UTC2TT(jd-timezone/24.0))
if hourAngle < 0 {
hourAngle += 360
}
return hourAngle
}
func JupiterCulminationTime(jde, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
func JupiterCulminationTime(localJD, lon, timezone float64) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5
estimateJD := jde + Limit360(360-JupiterHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 {
currentHourAngle := JupiterHourAngle(jde, lon, timezone)
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-JupiterHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := JupiterHourAngle(localJD, lon, timezone)
if currentHourAngle < 180 {
currentHourAngle += 360
}
+27 -27
View File
@@ -74,15 +74,15 @@ func jupiterConjunctionFull(jde, degree float64, next uint8) float64 {
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
estimateJDE := jde
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD
longitudeDelta := jupiterSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJD+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 {
prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJDE+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
@@ -90,7 +90,7 @@ func jupiterConjunctionFull(jde, degree float64, next uint8) float64 {
if !converged {
return math.NaN()
}
return TD2UT(estimateJD, false)
return TT2UTC(estimateJDE)
}
func jupiterConjunction(jde, degree float64, next uint8) float64 {
@@ -105,15 +105,15 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 {
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
estimateJDE := jde
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD
longitudeDelta := jupiterSunLongitudeDeltaN(prevJD, degree, true, jupiterEventSearchN)
longitudeSlope := (jupiterSunLongitudeDeltaN(prevJD+0.000005, degree, true, jupiterEventSearchN) - jupiterSunLongitudeDeltaN(prevJD-0.000005, degree, true, jupiterEventSearchN)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJD) <= jupiterPhaseCoarseTolerance {
prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDeltaN(prevJDE, degree, true, jupiterEventSearchN)
longitudeSlope := (jupiterSunLongitudeDeltaN(prevJDE+0.000005, degree, true, jupiterEventSearchN) - jupiterSunLongitudeDeltaN(prevJDE-0.000005, degree, true, jupiterEventSearchN)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= jupiterPhaseCoarseTolerance {
converged = true
break
}
@@ -123,12 +123,12 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 {
}
converged = false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD
longitudeDelta := jupiterSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJD+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 {
prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJDE+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
@@ -136,7 +136,7 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 {
if !converged {
return math.NaN()
}
return TD2UT(estimateJD, false)
return TT2UTC(estimateJDE)
}
func LastJupiterConjunction(jde float64) float64 {
@@ -175,22 +175,22 @@ func jupiterRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposit
if !isFiniteFloat(oppositionJD) {
return math.NaN()
}
oppositionTT := TD2UT(oppositionJD, true)
oppositionTT := UTC2TT(oppositionJD)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
easternQuadratureUT := jupiterConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
startTT = UTC2TT(easternQuadratureUT)
} else {
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)
}, func(jd float64) float64 {
return jupiterRADerivative(jd, stationDerivativeStepDay)
})
return TD2UT(bestJD, false)
return TT2UTC(bestJDE)
}
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.
func JupiterCentralMeridians(jd float64) JupiterCentralMeridianInfo {
return JupiterCentralMeridiansN(jd, -1)
func JupiterCentralMeridians(jde float64) JupiterCentralMeridianInfo {
return JupiterCentralMeridiansN(jde, -1)
}
// JupiterCentralMeridiansN 木星 System I/II/III 中央经线(截断版) / truncated Jupiter System I/II/III central meridians.
func JupiterCentralMeridiansN(jd float64, n int) JupiterCentralMeridianInfo {
observations := jupiterPhysicalObservationsN(jd, n)
physical := JupiterPhysicalN(jd, n)
func JupiterCentralMeridiansN(jde float64, n int) JupiterCentralMeridianInfo {
observations := jupiterPhysicalObservationsN(jde, n)
physical := JupiterPhysicalN(jde, n)
return JupiterCentralMeridianInfo{
SystemI: observations.SystemI,
SystemII: observations.SystemII,
@@ -41,18 +41,18 @@ func JupiterCentralMeridiansN(jd float64, n int) JupiterCentralMeridianInfo {
}
// JupiterDSDE 木星 DS/DE 行星中心赤纬 / Jupiter planetocentric declinations of Sun and Earth.
func JupiterDSDE(jd float64) (ds, de float64) {
return JupiterDSDEN(jd, -1)
func JupiterDSDE(jde float64) (ds, de float64) {
return JupiterDSDEN(jde, -1)
}
// JupiterDSDEN 木星 DS/DE 行星中心赤纬(截断版) / truncated Jupiter planetocentric declinations of Sun and Earth.
func JupiterDSDEN(jd float64, n int) (ds, de float64) {
observations := jupiterPhysicalObservationsN(jd, n)
func JupiterDSDEN(jde float64, n int) (ds, de float64) {
observations := jupiterPhysicalObservationsN(jde, n)
return observations.DS, observations.DE
}
func jupiterPhysicalObservationsN(jd float64, n int) jupiterPhysicalObservationInfo {
days := jd - 2433282.5
func jupiterPhysicalObservationsN(jde float64, n int) jupiterPhysicalObservationInfo {
days := jde - 2433282.5
julianCentury := days / 36525.0
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
w2 := (16.838 + 870.27003539*days) * rad
earthLon := planet.WherePlanetN(-1, 0, jd, n)
earthLat := planet.WherePlanetN(-1, 1, jd, n)
earthRadius := planet.WherePlanetN(-1, 2, jd, n)
earthLon := planet.WherePlanetN(-1, 0, jde, n)
earthLat := planet.WherePlanetN(-1, 1, jde, n)
earthRadius := planet.WherePlanetN(-1, 2, jde, n)
delta := 4.0
var jupiterLon float64
@@ -73,16 +73,16 @@ func jupiterPhysicalObservationsN(jd float64, n int) jupiterPhysicalObservationI
var z float64
for i := 0; i < 2; i++ {
lightTimeDays := astronomicalUnitLightTimeDays * delta
jupiterLon = planet.WherePlanetN(4, 0, jd-lightTimeDays, n)
jupiterLat = planet.WherePlanetN(4, 1, jd-lightTimeDays, n)
jupiterRadius = planet.WherePlanetN(4, 2, jd-lightTimeDays, n)
jupiterLon = planet.WherePlanetN(4, 0, jde-lightTimeDays, n)
jupiterLat = planet.WherePlanetN(4, 1, jde-lightTimeDays, n)
jupiterRadius = planet.WherePlanetN(4, 2, jde-lightTimeDays, n)
x = jupiterRadius*Cos(jupiterLat)*Cos(jupiterLon) - earthRadius*Cos(earthLat)*Cos(earthLon)
y = jupiterRadius*Cos(jupiterLat)*Sin(jupiterLon) - earthRadius*Cos(earthLat)*Sin(earthLon)
z = jupiterRadius*Sin(jupiterLat) - earthRadius*Sin(earthLat)
delta = math.Sqrt(x*x + y*y + z*z)
}
meanObliquity := EclipticObliquity(jd, false)
meanObliquity := EclipticObliquity(jde, false)
sinMeanObliquity, cosMeanObliquity := math.Sincos(meanObliquity)
sinJupiterLat, cosJupiterLat := math.Sincos(jupiterLat * rad)
sinJupiterLon, cosJupiterLon := math.Sincos(jupiterLon * rad)
+3 -3
View File
@@ -37,14 +37,14 @@ func TestJupiterCentralMeridianSystemIIIMatchesHorizonsBaseline(t *testing.T) {
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.InputUTC, err)
}
jd := TD2UT(Date2JDE(date.UTC()), true)
jd := UTC2TT(Date2JD(date.UTC()))
got := JupiterCentralMeridians(jd)
assertPlanetPhaseClose(t, "Jupiter."+sample.InputUTC+".CMIII", got.SystemIII, sample.SubEarthLongitude, 0.02)
}
}
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)
gotN := JupiterCentralMeridiansN(jd, -1)
ds, de := JupiterDSDE(jd)
@@ -76,7 +76,7 @@ func TestJupiterCentralMeridianAndDSDESampleSweepFiniteAndInRange(t *testing.T)
}
for _, date := range dates {
jd := TD2UT(Date2JDE(date.UTC()), true)
jd := UTC2TT(Date2JD(date.UTC()))
meridians := JupiterCentralMeridians(jd)
ds, de := JupiterDSDE(jd)
physical := JupiterPhysical(jd)
+6 -6
View File
@@ -464,8 +464,8 @@ func jupiterGalileanContactGeometryAt(
if !isFinite(jd) || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
return jupiterGalileanContactGeometry{}, false
}
evaluationJD := TD2UT(jd, true)
context := newJupiterGalileanObservationContext(evaluationJD)
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
if context.jupiterDistance == 0 {
return jupiterGalileanContactGeometry{}, false
}
@@ -527,8 +527,8 @@ func jupiterGalileanEclipseSignedDistanceAt(jd float64, satellite int, penumbra
if !isFinite(jd) || satellite < 1 || satellite > 4 {
return math.NaN(), false
}
evaluationJD := TD2UT(jd, true)
context := newJupiterGalileanObservationContext(evaluationJD)
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
if context.jupiterDistance == 0 {
return math.NaN(), false
}
@@ -590,8 +590,8 @@ func jupiterGalileanShadowLimbMetricAt(jd float64, satellite int, penumbra bool)
if !isFinite(jd) || satellite < 1 || satellite > 4 {
return math.NaN(), false
}
evaluationJD := TD2UT(jd, true)
context := newJupiterGalileanObservationContext(evaluationJD)
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
if context.jupiterDistance == 0 {
return math.NaN(), false
}
@@ -18,13 +18,13 @@ func TestJupiterGalileanPhenomenonContactEventsAgainstIMCCEBaseline(t *testing.T
queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2)
phenomenonType := parseBasicGalileanPhenomenonType(t, record.Type)
event := ClosestJupiterGalileanPhenomenonContactEvent(Date2JDE(queryMid.UTC()), record.Satellite, phenomenonType)
event := ClosestJupiterGalileanPhenomenonContactEvent(Date2JD(queryMid.UTC()), record.Satellite, phenomenonType)
if !event.Valid {
t.Fatalf("%s invalid contact event", record.Label)
}
gotStart := JDE2DateByZone(event.Disappearance.Start, time.UTC, false)
gotEnd := JDE2DateByZone(event.Reappearance.Start, time.UTC, false)
gotStart := JD2DateByZone(event.Disappearance.Start, time.UTC, false)
gotEnd := JD2DateByZone(event.Reappearance.Start, time.UTC, false)
startDiff := math.Abs(gotStart.Sub(startUTC).Seconds())
endDiff := math.Abs(gotEnd.Sub(endUTC).Seconds())
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) {
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 math.Abs(event.Disappearance.ModelCrossing-fullEvent.Start)*86400 > 2 {
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.
//
// jd 与返回时刻都是 UTC/UT 儒略日;需要瞬时状态时先用 TD2UT(jd, true) 换成 TT。
// jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func LastJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
return event
@@ -66,7 +66,7 @@ func LastJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonTyp
// 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 {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
return event
@@ -74,7 +74,7 @@ func NextJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonTyp
// 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 {
last, hasLast := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
next, hasNext := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
@@ -428,8 +428,8 @@ func jupiterGalileanPhenomenonMetricAt(
}
}
evaluationJD := TD2UT(jd, true)
context := newJupiterGalileanObservationContext(evaluationJD)
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
if context.jupiterDistance == 0 {
return jupiterGalileanMetricSample{
metric: math.Inf(1),
+5 -5
View File
@@ -33,9 +33,9 @@ func TestJupiterGalileanPhenomenonEventsAgainstIMCCEBaseline(t *testing.T) {
queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2)
phenomenonType := parseBasicGalileanPhenomenonType(t, record.Type)
next := NextJupiterGalileanPhenomenonEvent(Date2JDE(queryBefore.UTC()), record.Satellite, phenomenonType)
last := LastJupiterGalileanPhenomenonEvent(Date2JDE(queryAfter.UTC()), record.Satellite, phenomenonType)
closest := ClosestJupiterGalileanPhenomenonEvent(Date2JDE(queryMid.UTC()), record.Satellite, phenomenonType)
next := NextJupiterGalileanPhenomenonEvent(Date2JD(queryBefore.UTC()), record.Satellite, phenomenonType)
last := LastJupiterGalileanPhenomenonEvent(Date2JD(queryAfter.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+" last", last, record, startUTC, endUTC, &maxStartDiff, &maxEndDiff)
@@ -62,8 +62,8 @@ func assertGalileanEventMatchesBaseline(
if string(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)
gotEnd := JDE2DateByZone(event.End, time.UTC, false)
gotStart := JD2DateByZone(event.Start, time.UTC, false)
gotEnd := JD2DateByZone(event.End, time.UTC, false)
startDiff := math.Abs(gotStart.Sub(startUTC).Seconds())
endDiff := math.Abs(gotEnd.Sub(endUTC).Seconds())
if startDiff > *maxStartDiff {
+5 -5
View File
@@ -22,18 +22,18 @@ type JupiterGalileanPhenomenon struct {
}
// JupiterGalileanSatellitePhenomenon 单颗伽利略卫星瞬时现象 / instantaneous phenomena of one Galilean satellite.
func JupiterGalileanSatellitePhenomenon(jd float64, satellite int) JupiterGalileanPhenomenon {
if satellite < 1 || satellite > 4 || !isFinite(jd) {
func JupiterGalileanSatellitePhenomenon(jde float64, satellite int) JupiterGalileanPhenomenon {
if satellite < 1 || satellite > 4 || !isFinite(jde) {
return invalidJupiterGalileanPhenomenon()
}
context := newJupiterGalileanObservationContext(jd)
context := newJupiterGalileanObservationContext(jde)
return context.phenomenonForSatellite(satellite - 1)
}
// JupiterGalileanSatellitePhenomena 四颗伽利略卫星瞬时现象 / instantaneous phenomena of the four Galilean satellites.
func JupiterGalileanSatellitePhenomena(jd float64) [4]JupiterGalileanPhenomenon {
func JupiterGalileanSatellitePhenomena(jde float64) [4]JupiterGalileanPhenomenon {
var phenomena [4]JupiterGalileanPhenomenon
context := newJupiterGalileanObservationContext(jd)
context := newJupiterGalileanObservationContext(jde)
for i := range phenomena {
phenomena[i] = context.phenomenonForSatellite(i)
}
+20 -20
View File
@@ -35,7 +35,7 @@ type jupiterGalileanL1Term struct {
// JupiterGalileanState 木星伽利略卫星原始状态 / raw Galilean-satellite state.
//
// 输入 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 {
X float64
Y float64
@@ -97,11 +97,11 @@ func JupiterGalileanSatelliteStates(jd float64) [4]JupiterGalileanState {
//
// 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.
func JupiterGalileanSatelliteObservation(jd float64, satellite int) JupiterGalileanObservation {
if satellite < 1 || satellite > 4 || !isFinite(jd) {
func JupiterGalileanSatelliteObservation(jde float64, satellite int) JupiterGalileanObservation {
if satellite < 1 || satellite > 4 || !isFinite(jde) {
return invalidJupiterGalileanObservation()
}
context := newJupiterGalileanObservationContext(jd)
context := newJupiterGalileanObservationContext(jde)
return context.observationForSatellite(satellite - 1)
}
@@ -109,9 +109,9 @@ func JupiterGalileanSatelliteObservation(jd float64, satellite int) JupiterGalil
//
// 返回次序固定为 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
context := newJupiterGalileanObservationContext(jd)
context := newJupiterGalileanObservationContext(jde)
for i := range observations {
observations[i] = context.observationForSatellite(i)
}
@@ -141,14 +141,14 @@ type jupiterGalileanObservationContext struct {
bodyZ Vector3
}
func newJupiterGalileanObservationContext(jd float64) jupiterGalileanObservationContext {
context := jupiterGalileanObservationContext{jd: jd}
if !isFinite(jd) {
func newJupiterGalileanObservationContext(jde float64) jupiterGalileanObservationContext {
context := jupiterGalileanObservationContext{jd: jde}
if !isFinite(jde) {
return context
}
context.earthHelioJ2000 = rotateEclipticToEquatorial(earthHeliocentricVectorJ2000(jd), orbitJ2000Obliquity)
context.jupiterGeoJ2000, context.jupiterLightTime = jupiterAstrometricGeocentricVectorJ2000(jd, context.earthHelioJ2000)
context.targetJD = jd - context.jupiterLightTime
context.earthHelioJ2000 = rotateEclipticToEquatorial(earthHeliocentricVectorJ2000(jde), orbitJ2000Obliquity)
context.jupiterGeoJ2000, context.jupiterLightTime = jupiterAstrometricGeocentricVectorJ2000(jde, context.earthHelioJ2000)
context.targetJD = jde - context.jupiterLightTime
context.jupiterDistance = vectorMagnitude(context.jupiterGeoJ2000)
if context.jupiterDistance == 0 {
return context
@@ -220,9 +220,9 @@ func jupiterGalileanSatelliteAstrometricGeocentric(index int, jd, initialLightTi
result := Vector3{}
includeSolarLongPeriod := jupiterGalileanUseSolarLongPeriod(jd)
for i := 0; i < 8; i++ {
targetJD := jd - lightTime
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(targetJD), orbitJ2000Obliquity)
state = jupiterGalileanSatelliteStateAtET(targetJD-jupiterGalileanReferenceJD, index, includeSolarLongPeriod)
targetJDE := jd - lightTime
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(targetJDE), orbitJ2000Obliquity)
state = jupiterGalileanSatelliteStateAtET(targetJDE-jupiterGalileanReferenceJD, index, includeSolarLongPeriod)
result = Vector3{
jupiterHelio[0] + state.X - earthHelioJ2000[0],
jupiterHelio[1] + state.Y - earthHelioJ2000[1],
@@ -256,14 +256,14 @@ func jupiterAstrometricGeocentricVectorJ2000(jd float64, earthHelioJ2000 Vector3
return result, lightTime
}
func jupiterHeliocentricVectorJ2000(jd float64) Vector3 {
func jupiterHeliocentricVectorJ2000(jde float64) Vector3 {
return eclipticVectorAtReferenceEpoch(
eclipticCartesian(
planet.WherePlanet(4, 0, jd),
planet.WherePlanet(4, 1, jd),
planet.WherePlanet(4, 2, jd),
planet.WherePlanet(4, 0, jde),
planet.WherePlanet(4, 1, jde),
planet.WherePlanet(4, 2, jde),
),
jd,
jde,
orbitReferenceJD,
)
}
+24 -8
View File
@@ -71,7 +71,9 @@ func occultationPathVectorRaDec(vector occultationPathVector) (float64, float64,
if !finite(distance) || distance <= 0 {
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
return ra, dec, distance, finite(ra) && finite(dec)
}
@@ -112,6 +114,9 @@ type starOccultationLocalEphemeris struct {
star StarCoordinate
nodes []localEphemerisVectorNode
dense bool
// distanceKM 是中心时刻的当日距离;hasDistance 为假表示恒星距离未知,几何按无穷远处理。
distanceKM float64
hasDistance bool
}
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},
}
}
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) {
@@ -144,15 +158,20 @@ func (ephemeris *starOccultationLocalEphemeris) stateAt(tt float64) (starOcculta
moonRA, moonDec, moonDistance, moonOK := occultationPathVectorRaDec(occultationPathVector{
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],
})
if !moonOK || !targetOK {
return starOccultationEphemerisState{}, false
}
// 距离取插值矢量自身的模长,才与同一次插值给出的方向同源;距离未知时按 0 上报。
starDistanceKM := 0.0
if ephemeris.hasDistance {
starDistanceKM = targetDistance
}
return starOccultationEphemerisState{
moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistance,
starRA: targetRA, starDec: targetDec, starDistanceKM: ephemeris.starDistanceKM(),
starRA: targetRA, starDec: targetDec, starDistanceKM: starDistanceKM,
valid: true,
}, true
}
@@ -168,10 +187,7 @@ func (ephemeris *starOccultationLocalEphemeris) vectorsAt(tt float64) ([3]float6
}
func (ephemeris *starOccultationLocalEphemeris) starDistanceKM() float64 {
if ephemeris.star.ParallaxMas <= 0 {
return 0
}
return 206264806.247 / ephemeris.star.ParallaxMas * occultationPathAstronomicalUnitKM
return ephemeris.distanceKM
}
type planetOccultationLocalEphemeris struct {
+27 -5
View File
@@ -11,11 +11,11 @@ func TestSolarEclipseLocalEphemerisBoundedError(t *testing.T) {
name string
seed float64
}{
{"2010-01-15", JDECalc(2010, 1, 15)},
{"2014-04-29", JDECalc(2014, 4, 29)},
{"2023-04-20", JDECalc(2023, 4, 20)},
{"2031-05-21", JDECalc(2031, 5, 21)},
{"2309-06-09", JDECalc(2309, 6, 9)},
{"2010-01-15", JDCalc(2010, 1, 15)},
{"2014-04-29", JDCalc(2014, 4, 29)},
{"2023-04-20", JDCalc(2023, 4, 20)},
{"2031-05-21", JDCalc(2031, 5, 21)},
{"2309-06-09", JDCalc(2309, 6, 9)},
}
for _, event := range events {
t.Run(event.name, func(t *testing.T) {
@@ -133,3 +133,25 @@ func TestOccultationLocalEphemerisFullWindowBoundedError(t *testing.T) {
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]))
}
// 矢量回读的赤经必须落在 [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 用于月亮视半径和半影几何
// - 959.63 / 8.794 分别为太阳视半径与太阳视差的常用角秒常量
// - 太阳视半径与日食共用标准档常量,太阳视差用常用角秒常量 8.794
lunarMoonRadiusRatio = 0.2725076
lunarMoonRadiusScale = lunarMoonRadiusRatio * lunarEarthEquatorialRadiusKM * 1.0000036
lunarSolarRadiusArcsec = 959.63
lunarSolarParallaxArcsec = 8.794
lunarLongitudeAberration = -3.4e-6
lunarFiniteDifferenceStep = 60.0 / 86400.0
@@ -336,7 +335,7 @@ func computeLunarShadowState(jde float64, shadowModel lunarEclipseShadowModel) l
moonRadiusArcsec := lunarMoonRadiusScale * lunarArcsecPerRadian / moonDistanceKM
earthParallaxArcsec := lunarEarthEquatorialRadiusKM / moonDistanceKM * lunarArcsecPerRadian
solarRadiusArcsec := lunarSolarRadiusArcsec / sunDistanceAU
solarRadiusArcsec := eclipseSunRadiusStandardArcsec / sunDistanceAU
solarParallaxArcsec := lunarSolarParallaxArcsec / sunDistanceAU
umbraRadiusArcsec, penumbraRadiusArcsec := lunarEclipseShadowRadiiArcsec(
earthParallaxArcsec,
+3 -3
View File
@@ -38,7 +38,7 @@ func TestLunarEclipseAbsentPhasesAreNaN(t *testing.T) {
func TestLunarEclipseWithoutEclipseHasNoContacts(t *testing.T) {
// 2025-01-13 的望月没有月食(半影食分 < 0)。
eclipse := LunarEclipse(JDECalc(2025, 1, 13))
eclipse := LunarEclipse(JDCalc(2025, 1, 13))
if 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) {
eclipse := LunarEclipse(JDECalc(2025, 3, 14))
eclipse := LunarEclipse(JDCalc(2025, 3, 14))
if eclipse.Type != LunarEclipseTotal || !eclipse.HasPenumbral || !eclipse.HasPartial || !eclipse.HasTotal {
t.Fatalf("unexpected result: %+v", eclipse)
}
@@ -92,7 +92,7 @@ func TestLunarEclipseDiagramSkipsAbsentPhases(t *testing.T) {
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))
}
}
+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.
type LunarEclipseShadowGeometry struct {
// Gamma 是月心到地影轴的最小距离,单位地球赤道半径。
// Gamma is the least distance from the Moon's centre to the shadow axis, in Earth equatorial radii.
Gamma float64
// PenumbralRadiusDegrees 与 UmbralRadiusDegrees 是半影、本影在地影轴垂直面上的角半径,单位度。
// PenumbralRadiusDegrees and UmbralRadiusDegrees are the penumbral and umbral angular radii on the
// plane perpendicular to the shadow axis, in degrees.
PenumbralRadiusDegrees float64
UmbralRadiusDegrees float64
// MoonDistanceEarthRadii 是食甚时的地心月距。
// MoonDistanceEarthRadii 是食甚时的地心月距,单位地球赤道半径。
// MoonDistanceEarthRadii is the geocentric lunar distance at greatest eclipse, in Earth equatorial radii.
MoonDistanceEarthRadii float64
// AxisDegrees 是食甚时月心到地影轴的角距,即 NASA 月食图上那列 Axis。
// 它与 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(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)
// 影几何内部用的是小角近似的地球视差(R⊕/月距),这里必须用同一形式,否则二级差会露出来。
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(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)
moonSemidiameter := MoonSemidiameter(result.Maximum) / 3600
fromUmbral := geometry.UmbralRadiusDegrees + moonSemidiameter -
+17 -17
View File
@@ -27,7 +27,7 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
testCases := []lunarEclipseBaseline{
{
name: "2022-11-08 total",
jde: JDECalc(2022, 11, 8),
jde: JDCalc(2022, 11, 8),
expectedType: LunarEclipseTotal,
expectedMax: 2459891.9585873615,
expectedMag: 1.3635170051692678,
@@ -40,14 +40,14 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
},
{
name: "2023-05-05 penumbral",
jde: JDECalc(2023, 5, 5),
jde: JDCalc(2023, 5, 5),
expectedType: LunarEclipsePenumbral,
expectedPenumbralStart: 2460070.1342392800,
expectedPenumbralEnd: 2460070.3159191823,
},
{
name: "2023-10-28 partial",
jde: JDECalc(2023, 10, 28),
jde: JDCalc(2023, 10, 28),
expectedType: LunarEclipsePartial,
expectedMax: 2460246.3439460830,
expectedMag: 0.12723850274626405,
@@ -58,14 +58,14 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
},
{
name: "2024-03-25 penumbral",
jde: JDECalc(2024, 3, 25),
jde: JDCalc(2024, 3, 25),
expectedType: LunarEclipsePenumbral,
expectedPenumbralStart: 2460394.7028870000,
expectedPenumbralEnd: 2460394.8999071894,
},
{
name: "2024-09-18 partial",
jde: JDECalc(2024, 9, 18),
jde: JDCalc(2024, 9, 18),
expectedType: LunarEclipsePartial,
expectedMax: 2460571.6148748010,
expectedMag: 0.09042791952817894,
@@ -76,7 +76,7 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
},
{
name: "2025-03-14 total",
jde: JDECalc(2025, 3, 14),
jde: JDCalc(2025, 3, 14),
expectedType: LunarEclipseTotal,
expectedMax: 2460748.7916214615,
expectedMag: 1.1828107517800281,
@@ -89,7 +89,7 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
},
{
name: "2025-09-07 total",
jde: JDECalc(2025, 9, 7),
jde: JDCalc(2025, 9, 7),
expectedType: LunarEclipseTotal,
expectedMax: 2460926.2590034613,
expectedMag: 1.3672329695760280,
@@ -102,7 +102,7 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
},
{
name: "2026-03-03 total",
jde: JDECalc(2026, 3, 3),
jde: JDCalc(2026, 3, 3),
expectedType: LunarEclipseTotal,
expectedMax: 2461102.9825476190,
expectedMag: 1.1556387222746651,
@@ -154,7 +154,7 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
}
func TestLunarEclipseDefaultUsesDanjon(t *testing.T) {
jde := JDECalc(2025, 3, 14)
jde := JDCalc(2025, 3, 14)
defaultResult := LunarEclipse(jde)
danjonResult := LunarEclipseDanjon(jde)
chauvenetResult := LunarEclipseChauvenet(jde)
@@ -176,9 +176,9 @@ func TestPenumbralLunarEclipseKeepsNegativeUmbralMagnitude(t *testing.T) {
jde float64
calc func(float64) LunarEclipseResult
}{
{name: "default 2024-03-25", jde: JDECalc(2024, 3, 25), calc: LunarEclipse},
{name: "danjon 2024-03-25", jde: JDECalc(2024, 3, 25), calc: LunarEclipseDanjon},
{name: "chauvenet 2023-05-05", jde: JDECalc(2023, 5, 5), calc: LunarEclipseChauvenet},
{name: "default 2024-03-25", jde: JDCalc(2024, 3, 25), calc: LunarEclipse},
{name: "danjon 2024-03-25", jde: JDCalc(2024, 3, 25), calc: LunarEclipseDanjon},
{name: "chauvenet 2023-05-05", jde: JDCalc(2023, 5, 5), calc: LunarEclipseChauvenet},
}
for _, tc := range testCases {
@@ -210,28 +210,28 @@ func TestLunarEclipseDanjonMagnitudesCloserToNASA(t *testing.T) {
}{
{
name: "2023-10-28 partial",
jde: JDECalc(2023, 10, 28),
jde: JDCalc(2023, 10, 28),
expectedType: LunarEclipsePartial,
nasaPenumbralMagnitude: 1.1181,
nasaUmbralMagnitude: 0.1220,
},
{
name: "2025-03-14 total",
jde: JDECalc(2025, 3, 14),
jde: JDCalc(2025, 3, 14),
expectedType: LunarEclipseTotal,
nasaPenumbralMagnitude: 2.2595,
nasaUmbralMagnitude: 1.1784,
},
{
name: "2026-03-03 total",
jde: JDECalc(2026, 3, 3),
jde: JDCalc(2026, 3, 3),
expectedType: LunarEclipseTotal,
nasaPenumbralMagnitude: 2.1838,
nasaUmbralMagnitude: 1.1507,
},
{
name: "2026-08-28 partial",
jde: JDECalc(2026, 8, 28),
jde: JDCalc(2026, 8, 28),
expectedType: LunarEclipsePartial,
nasaPenumbralMagnitude: 1.9645,
nasaUmbralMagnitude: 0.9299,
@@ -277,7 +277,7 @@ func TestLunarEclipseNoEvent(t *testing.T) {
for _, tc := range testCases {
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 {
t.Fatalf("Type mismatch: got %s want %s", result.Type, LunarEclipseNone)
}
+3 -5
View File
@@ -3,16 +3,14 @@ package basic
import "math"
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
adjustedYear := year
if month == 11 || month == 12 {
winterSolsticeDay := GetJQTime(year, 270) + tz
//firstNewMoonDay := TD2UT(CalcMoonS(float64(year)+11.0/12.0+5.0/30.0/12.0, 0), true) + tz
//nextNewMoonDay := TD2UT(CalcMoonS(float64(year)+1.0, 0), true) + tz
firstNewMoonDay := TD2UT(CalcMoonSHByJDE(winterSolsticeDay-16, 0), false) + tz
nextNewMoonDay := TD2UT(CalcMoonSHByJDE(firstNewMoonDay+28, 0), false) + tz
firstNewMoonDay := TT2UTC(CalcMoonSHByJDE(winterSolsticeDay-16, 0)) + tz
nextNewMoonDay := TT2UTC(CalcMoonSHByJDE(firstNewMoonDay+28, 0)) + tz
firstNewMoonDay = normalizeTimePoint(firstNewMoonDay)
nextNewMoonDay = normalizeTimePoint(nextNewMoonDay)
+63 -63
View File
@@ -7,79 +7,79 @@ import (
. "b612.me/astro/tools"
)
func MarsL(jd float64) float64 {
return planet.WherePlanet(3, 0, jd)
func MarsL(jde float64) float64 {
return planet.WherePlanet(3, 0, jde)
}
func MarsB(jd float64) float64 {
return planet.WherePlanet(3, 1, jd)
func MarsB(jde float64) float64 {
return planet.WherePlanet(3, 1, jde)
}
func MarsR(jd float64) float64 {
return planet.WherePlanet(3, 2, jd)
func MarsR(jde float64) float64 {
return planet.WherePlanet(3, 2, jde)
}
func AMarsX(jd float64) float64 {
l := MarsL(jd)
b := MarsB(jd)
r := MarsR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func AMarsX(jde float64) float64 {
l := MarsL(jde)
b := MarsB(jde)
r := MarsR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x
}
func AMarsY(jd float64) float64 {
func AMarsY(jde float64) float64 {
l := MarsL(jd)
b := MarsB(jd)
r := MarsR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
l := MarsL(jde)
b := MarsB(jde)
r := MarsR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y
}
func AMarsZ(jd float64) float64 {
//l := MarsL(jd)
b := MarsB(jd)
r := MarsR(jd)
// el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func AMarsZ(jde float64) float64 {
//l := MarsL(jde)
b := MarsB(jde)
r := MarsR(jde)
// el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
z := r*Sin(b) - er*Sin(eb)
return z
}
func AMarsXYZ(jd float64) (float64, float64, float64) {
l := MarsL(jd)
b := MarsB(jd)
r := MarsR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func AMarsXYZ(jde float64) (float64, float64, float64) {
l := MarsL(jde)
b := MarsB(jde)
r := MarsR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb)
return x, y, z
}
func MarsApparentRa(jd float64) float64 {
lo, bo := MarsApparentLoBo(jd)
eps := TrueObliquity(jd)
func MarsApparentRa(jde float64) float64 {
lo, bo := MarsApparentLoBo(jde)
eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
return Limit360(ra)
}
func MarsApparentDec(jd float64) float64 {
lo, bo := MarsApparentLoBo(jd)
eps := TrueObliquity(jd)
func MarsApparentDec(jde float64) float64 {
lo, bo := MarsApparentLoBo(jde)
eps := TrueObliquity(jde)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec
}
func MarsApparentRaDec(jd float64) (float64, float64) {
lo, bo := MarsApparentLoBo(jd)
eps := TrueObliquity(jd)
func MarsApparentRaDec(jde float64) (float64, float64) {
lo, bo := MarsApparentLoBo(jde)
eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
@@ -115,22 +115,22 @@ func MarsTrueLo(jd float64) float64 {
return geo.lo
}
func MarsMag(jd float64) float64 {
sunDistance := MarsR(jd)
earthDistance := EarthMarsAway(jd)
earthSunDistance := planet.WherePlanet(-1, 2, jd)
func MarsMag(jde float64) float64 {
sunDistance := MarsR(jde)
earthDistance := EarthMarsAway(jde)
earthSunDistance := planet.WherePlanet(-1, 2, jde)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(i)
mag := -1.52 + 5*math.Log10(sunDistance*earthDistance) + 0.016*i
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))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := MarsApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
@@ -139,12 +139,12 @@ func MarsHeight(jde, lon, lat, timezone float64) float64 {
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))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := MarsApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 三角转换公式
@@ -163,21 +163,21 @@ func MarsAzimuth(jde, lon, lat, timezone float64) float64 {
}
func MarsHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon)
hourAngle := siderealLongitude - MarsApparentRa(TD2UT(jd-timezone/24.0, true))
siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - MarsApparentRa(UTC2TT(jd-timezone/24.0))
if hourAngle < 0 {
hourAngle += 360
}
return hourAngle
}
func MarsCulminationTime(jde, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
func MarsCulminationTime(localJD, lon, timezone float64) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5
estimateJD := jde + Limit360(360-MarsHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 {
currentHourAngle := MarsHourAngle(jde, lon, timezone)
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-MarsHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := MarsHourAngle(localJD, lon, timezone)
if currentHourAngle < 180 {
currentHourAngle += 360
}
+1 -1
View File
@@ -28,7 +28,7 @@ func marsEventSamples() []time.Time {
}
func marsEventSampleTTJD(date time.Time) float64 {
return TD2UT(Date2JDE(date.UTC()), true)
return UTC2TT(Date2JD(date.UTC()))
}
func marsEventCases() []marsEventCase {
+27 -27
View File
@@ -77,15 +77,15 @@ func marsConjunctionFull(jde, degree float64, next uint8) float64 {
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
estimateJDE := jde
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD
longitudeDelta := marsSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJD+0.000005, degree, true) - marsSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 {
prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJDE+0.000005, degree, true) - marsSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
@@ -93,7 +93,7 @@ func marsConjunctionFull(jde, degree float64, next uint8) float64 {
if !converged {
return math.NaN()
}
return TD2UT(estimateJD, false)
return TT2UTC(estimateJDE)
}
func marsConjunction(jde, degree float64, next uint8) float64 {
@@ -108,15 +108,15 @@ func marsConjunction(jde, degree float64, next uint8) float64 {
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
estimateJDE := jde
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD
longitudeDelta := marsSunLongitudeDeltaN(prevJD, degree, true, marsEventSearchN)
longitudeSlope := (marsSunLongitudeDeltaN(prevJD+0.000005, degree, true, marsEventSearchN) - marsSunLongitudeDeltaN(prevJD-0.000005, degree, true, marsEventSearchN)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJD) <= marsPhaseCoarseTolerance {
prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDeltaN(prevJDE, degree, true, marsEventSearchN)
longitudeSlope := (marsSunLongitudeDeltaN(prevJDE+0.000005, degree, true, marsEventSearchN) - marsSunLongitudeDeltaN(prevJDE-0.000005, degree, true, marsEventSearchN)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= marsPhaseCoarseTolerance {
converged = true
break
}
@@ -126,12 +126,12 @@ func marsConjunction(jde, degree float64, next uint8) float64 {
}
converged = false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD
longitudeDelta := marsSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJD+0.000005, degree, true) - marsSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 {
prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJDE+0.000005, degree, true) - marsSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
@@ -139,7 +139,7 @@ func marsConjunction(jde, degree float64, next uint8) float64 {
if !converged {
return math.NaN()
}
return TD2UT(estimateJD, false)
return TT2UTC(estimateJDE)
}
func LastMarsConjunction(jde float64) float64 {
@@ -178,22 +178,22 @@ func marsRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition
if !isFiniteFloat(oppositionJD) {
return math.NaN()
}
oppositionTT := TD2UT(oppositionJD, true)
oppositionTT := UTC2TT(oppositionJD)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
easternQuadratureUT := marsConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
startTT = UTC2TT(easternQuadratureUT)
} else {
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)
}, func(jd float64) float64 {
return marsRADerivative(jd, marsStationDerivativeStepDay)
})
return TD2UT(bestJD, false)
return TT2UTC(bestJDE)
}
func NextMarsRetrogradeToPrograde(jde float64) float64 {
+1 -1
View File
@@ -27,7 +27,7 @@ func BenchmarkTrueObliquity(b *testing.B) {
var benchMoonRise, benchMoonSet float64
func BenchmarkMoonRiseSetChain(b *testing.B) {
jd := JDECalc(2023, 6, 21)
jd := JDCalc(2023, 6, 21)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
benchMoonRise, _ = GetMoonRiseTime(jd, 116.4074, 39.9042, 8, 1, 0)
+63 -63
View File
@@ -7,76 +7,76 @@ import (
. "b612.me/astro/tools"
)
func MercuryL(jd float64) float64 {
return planet.WherePlanet(1, 0, jd)
func MercuryL(jde float64) float64 {
return planet.WherePlanet(1, 0, jde)
}
func MercuryB(jd float64) float64 {
return planet.WherePlanet(1, 1, jd)
func MercuryB(jde float64) float64 {
return planet.WherePlanet(1, 1, jde)
}
func MercuryR(jd float64) float64 {
return planet.WherePlanet(1, 2, jd)
func MercuryR(jde float64) float64 {
return planet.WherePlanet(1, 2, jde)
}
func AMercuryX(jd float64) float64 {
l := MercuryL(jd)
b := MercuryB(jd)
r := MercuryR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func AMercuryX(jde float64) float64 {
l := MercuryL(jde)
b := MercuryB(jde)
r := MercuryR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x
}
func AMercuryY(jd float64) float64 {
func AMercuryY(jde float64) float64 {
l := MercuryL(jd)
b := MercuryB(jd)
r := MercuryR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
l := MercuryL(jde)
b := MercuryB(jde)
r := MercuryR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y
}
func AMercuryZ(jd float64) float64 {
//l := MercuryL(jd)
b := MercuryB(jd)
r := MercuryR(jd)
// el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func AMercuryZ(jde float64) float64 {
//l := MercuryL(jde)
b := MercuryB(jde)
r := MercuryR(jde)
// el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
z := r*Sin(b) - er*Sin(eb)
return z
}
func AMercuryXYZ(jd float64) (float64, float64, float64) {
l := MercuryL(jd)
b := MercuryB(jd)
r := MercuryR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func AMercuryXYZ(jde float64) (float64, float64, float64) {
l := MercuryL(jde)
b := MercuryB(jde)
r := MercuryR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb)
return x, y, z
}
func MercuryApparentRa(jd float64) float64 {
lo, bo := MercuryApparentLoBo(jd)
return LoToRa(jd, lo, bo)
func MercuryApparentRa(jde float64) float64 {
lo, bo := MercuryApparentLoBo(jde)
return LoToRa(jde, lo, bo)
}
func MercuryApparentDec(jd float64) float64 {
lo, bo := MercuryApparentLoBo(jd)
eps := TrueObliquity(jd)
func MercuryApparentDec(jde float64) float64 {
lo, bo := MercuryApparentLoBo(jde)
eps := TrueObliquity(jde)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec
}
func MercuryApparentRaDec(jd float64) (float64, float64) {
lo, bo := MercuryApparentLoBo(jd)
return LoBoToRaDec(jd, lo, bo)
func MercuryApparentRaDec(jde float64) (float64, float64) {
lo, bo := MercuryApparentLoBo(jde)
return LoBoToRaDec(jde, lo, bo)
}
func EarthMercuryAway(jd float64) float64 {
@@ -98,22 +98,22 @@ func MercuryApparentLoBo(jd float64) (float64, float64) {
return geo.lo, geo.bo
}
func MercuryMag(jd float64) float64 {
sunDistance := MercuryR(jd)
earthDistance := EarthMercuryAway(jd)
earthSunDistance := planet.WherePlanet(-1, 2, jd)
func MercuryMag(jde float64) float64 {
sunDistance := MercuryR(jde)
earthDistance := EarthMercuryAway(jde)
earthSunDistance := planet.WherePlanet(-1, 2, jde)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(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)
}
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))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := MercuryApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
@@ -122,12 +122,12 @@ func MercuryHeight(jde, lon, lat, timezone float64) float64 {
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))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := MercuryApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 三角转换公式
@@ -146,21 +146,21 @@ func MercuryAzimuth(jde, lon, lat, timezone float64) float64 {
}
func MercuryHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon)
hourAngle := siderealLongitude - MercuryApparentRa(TD2UT(jd-timezone/24.0, true))
siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - MercuryApparentRa(UTC2TT(jd-timezone/24.0))
if hourAngle < 0 {
hourAngle += 360
}
return hourAngle
}
func MercuryCulminationTime(jde, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
func MercuryCulminationTime(localJD, lon, timezone float64) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5
estimateJD := jde + Limit360(360-MercuryHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 {
currentHourAngle := MercuryHourAngle(jde, lon, timezone)
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-MercuryHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := MercuryHourAngle(localJD, lon, timezone)
if currentHourAngle < 180 {
currentHourAngle += 360
}
+1 -1
View File
@@ -31,7 +31,7 @@ func mercuryEventSamples() []time.Time {
}
func mercuryEventSampleTTJD(date time.Time) float64 {
return TD2UT(Date2JDE(date.UTC()), true)
return UTC2TT(Date2JD(date.UTC()))
}
func mercuryEventCases() []mercuryEventCase {
+56 -56
View File
@@ -51,11 +51,11 @@ type mercuryConjunctionResult struct {
geoLightDays float64
}
func mercuryHelioN(planetIndex int, jd float64, n int) mercuryConjunctionLBR {
func mercuryHelioN(planetIndex int, jde float64, n int) mercuryConjunctionLBR {
return mercuryConjunctionLBR{
lo: planet.WherePlanetN(planetIndex, 0, jd, n),
bo: planet.WherePlanetN(planetIndex, 1, jd, n),
r: planet.WherePlanetN(planetIndex, 2, jd, n),
lo: planet.WherePlanetN(planetIndex, 0, jde, n),
bo: planet.WherePlanetN(planetIndex, 1, jde, n),
r: planet.WherePlanetN(planetIndex, 2, jde, n),
}
}
@@ -82,9 +82,9 @@ func mercuryConjunctionAngleDelta(diff float64) float64 {
return diff
}
func mercuryConjunctionHeliocentricDelta(jd, targetDeg float64, n int) float64 {
planetLo := planet.WherePlanetN(1, 0, jd, n)
earthLo := planet.WherePlanetN(-1, 0, jd, n)
func mercuryConjunctionHeliocentricDelta(jde, targetDeg float64, n int) float64 {
planetLo := planet.WherePlanetN(1, 0, jde, n)
earthLo := planet.WherePlanetN(-1, 0, jde, n)
return mercuryConjunctionAngleDelta(planetLo - earthLo - targetDeg)
}
@@ -101,8 +101,8 @@ func mercuryConjunctionDifference(jd float64, n int, targetDeg, sunLightDays, ge
}
}
func mercuryConjunctionExactDelta(jd float64) float64 {
return mercuryConjunctionAngleDelta(MercuryApparentLo(jd) - HSunApparentLo(jd))
func mercuryConjunctionExactDelta(jde float64) float64 {
return mercuryConjunctionAngleDelta(MercuryApparentLo(jde) - HSunApparentLo(jde))
}
func mercuryConjunctionApproxTT(seed float64, inferior bool) float64 {
@@ -110,32 +110,32 @@ func mercuryConjunctionApproxTT(seed float64, inferior bool) float64 {
if inferior {
heliocentricTarget = 0
}
jd := seed
jde := seed
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)
nextSample := mercuryConjunctionDifference(jd+mercuryConjunctionDerivativeStepDay, 8, 0, 0, 0)
startSample := mercuryConjunctionDifference(jde, 8, 0, 0, 0)
nextSample := mercuryConjunctionDifference(jde+mercuryConjunctionDerivativeStepDay, 8, 0, 0, 0)
diffSlope := mercuryConjunctionAngleDelta(nextSample.diff-startSample.diff) / mercuryConjunctionDerivativeStepDay
refined := mercuryConjunctionDifference(jd, 40, 0, startSample.sunLightDays, startSample.geoLightDays)
jd -= refined.diff / diffSlope
final := mercuryConjunctionDifference(jd, -1, 0, refined.sunLightDays, refined.geoLightDays)
jd -= final.diff / diffSlope
return jd
refined := mercuryConjunctionDifference(jde, 40, 0, startSample.sunLightDays, startSample.geoLightDays)
jde -= refined.diff / diffSlope
final := mercuryConjunctionDifference(jde, -1, 0, refined.sunLightDays, refined.geoLightDays)
jde -= final.diff / diffSlope
return jde
}
func mercuryConjunctionExactTT(seed float64, inferior bool) float64 {
estimateJD := mercuryConjunctionApproxTT(seed, inferior)
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD
longitudeDelta := mercuryConjunctionExactDelta(prevJD)
longitudeSlope := (mercuryConjunctionExactDelta(prevJD+0.000005) - mercuryConjunctionExactDelta(prevJD-0.000005)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope
prevJDE := estimateJD
longitudeDelta := mercuryConjunctionExactDelta(prevJDE)
longitudeSlope := (mercuryConjunctionExactDelta(prevJDE+0.000005) - mercuryConjunctionExactDelta(prevJDE-0.000005)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 {
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
@@ -161,7 +161,7 @@ func mercuryConjunction(jde float64, next uint8) float64 {
if math.Abs(mercuryConjunctionExactDelta(jde)) <= mercuryConjunctionSameInstantDegrees {
best := math.NaN()
consider := func(inferior bool) {
eventUT := TD2UT(mercuryConjunctionExactTT(jde, inferior), false)
eventUT := TT2UTC(mercuryConjunctionExactTT(jde, inferior))
if !isFiniteFloat(eventUT) {
return
}
@@ -186,80 +186,80 @@ func mercuryConjunction(jde float64, next uint8) float64 {
if next == 0 {
direction = -1
}
leftJD := jde
leftValue := mercuryConjunctionDeltaN(leftJD, mercuryEventSearchN)
leftJDE := jde
leftValue := mercuryConjunctionDeltaN(leftJDE, mercuryEventSearchN)
if !isFiniteFloat(leftValue) {
return math.NaN()
}
for i := 0; i < mercuryConjunctionScanMaxSteps; i++ {
rightJD := jde + direction*mercuryConjunctionScanStepDay*float64(i+1)
rightValue := mercuryConjunctionDeltaN(rightJD, mercuryEventSearchN)
rightJDE := jde + direction*mercuryConjunctionScanStepDay*float64(i+1)
rightValue := mercuryConjunctionDeltaN(rightJDE, mercuryEventSearchN)
if !isFiniteFloat(rightValue) {
return math.NaN()
}
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()
}
// mercuryConjunctionDeltaN 截断级数下的水星-太阳视黄经差(度,[-180,180]),用于方向性括号扫描。
func mercuryConjunctionDeltaN(jd float64, n int) float64 {
return mercuryConjunctionAngleDelta(MercuryApparentLoN(jd, n) - HSunApparentLoN(jd, n))
func mercuryConjunctionDeltaN(jde float64, n int) float64 {
return mercuryConjunctionAngleDelta(MercuryApparentLoN(jde, n) - HSunApparentLoN(jde, n))
}
// mercuryConjunctionPolish 用全项级数在截断级数给出的括号内抛光。
// 截断误差可能让括号两端在全项函数上同号(罕见),此时沿扫描方向再扩一两个扫描步;
// 若仍未被确认(典型情形:查询几乎正好落在合上,截断级数在根两侧的符号与全项不一致),
// 退回全项级数的方向扫描,保证有界且不返回 NaN。
func mercuryConjunctionPolish(jde, leftJD, rightJD, direction float64) float64 {
func mercuryConjunctionPolish(jde, leftJDE, rightJDE, direction float64) float64 {
for attempt := 0; attempt < 3; attempt++ {
leftValue := mercuryConjunctionExactDelta(leftJD)
rightValue := mercuryConjunctionExactDelta(rightJD)
leftValue := mercuryConjunctionExactDelta(leftJDE)
rightValue := mercuryConjunctionExactDelta(rightJDE)
if !isFiniteFloat(leftValue) || !isFiniteFloat(rightValue) {
return math.NaN()
}
if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 {
root, ok := eventBracketSecantRoot(leftJD, rightJD, leftValue, rightValue,
root, ok := eventBracketSecantRoot(leftJDE, rightJDE, leftValue, rightValue,
mercuryConjunctionPolishToleranceDay, mercuryConjunctionExactDelta)
if !ok {
return math.NaN()
}
return TD2UT(root, false)
return TT2UTC(root)
}
if direction > 0 {
rightJD += mercuryConjunctionScanStepDay
rightJDE += mercuryConjunctionScanStepDay
continue
}
leftJD -= mercuryConjunctionScanStepDay
leftJDE -= mercuryConjunctionScanStepDay
}
return mercuryConjunctionFullDirectionalScan(jde, direction)
}
// mercuryConjunctionFullDirectionalScan 全项级数的方向扫描(截断括号未被确认时的兜底)。
func mercuryConjunctionFullDirectionalScan(jde, direction float64) float64 {
leftJD := jde
leftValue := mercuryConjunctionExactDelta(leftJD)
leftJDE := jde
leftValue := mercuryConjunctionExactDelta(leftJDE)
if !isFiniteFloat(leftValue) {
return math.NaN()
}
for i := 0; i < mercuryConjunctionScanMaxSteps; i++ {
rightJD := jde + direction*mercuryConjunctionScanStepDay*float64(i+1)
rightValue := mercuryConjunctionExactDelta(rightJD)
rightJDE := jde + direction*mercuryConjunctionScanStepDay*float64(i+1)
rightValue := mercuryConjunctionExactDelta(rightJDE)
if !isFiniteFloat(rightValue) {
return math.NaN()
}
if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 {
root, ok := eventBracketSecantRoot(leftJD, rightJD, leftValue, rightValue,
root, ok := eventBracketSecantRoot(leftJDE, rightJDE, leftValue, rightValue,
mercuryConjunctionPolishToleranceDay, mercuryConjunctionExactDelta)
if !ok {
return math.NaN()
}
return TD2UT(root, false)
return TT2UTC(root)
}
leftJD, leftValue = rightJD, rightValue
leftJDE, leftValue = rightJDE, rightValue
}
return math.NaN()
}
@@ -335,12 +335,12 @@ func mercuryRADerivativeN(jde, delta float64, n int) 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)
}, func(jd float64) float64 {
return mercuryRADerivative(jd, mercuryStationDerivativeStepDay)
})
return TD2UT(bestJD, false)
return TT2UTC(bestJDE)
}
func mercuryStationBetween(startTT, endTT float64) bool {
@@ -377,12 +377,12 @@ func mercuryStationBetween(startTT, endTT float64) bool {
}
func mercuryProgradeToRetrogradeAroundInferior(inferiorUT float64) float64 {
inferiorTT := TD2UT(inferiorUT, true)
inferiorTT := UTC2TT(inferiorUT)
return mercuryStationInWindow(inferiorTT-mercuryStationWindowDays, inferiorTT)
}
func mercuryRetrogradeToProgradeAroundInferior(inferiorUT float64) float64 {
inferiorTT := TD2UT(inferiorUT, true)
inferiorTT := UTC2TT(inferiorUT)
return mercuryStationInWindow(inferiorTT, inferiorTT+mercuryStationWindowDays)
}
@@ -488,7 +488,7 @@ func NextMercuryRetrograde(jde float64) float64 {
motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay)
if motion > mercuryStationMotionTolerance {
p2r := NextMercuryProgradeToRetrograde(jde)
if isFiniteFloat(p2r) && !mercuryStationBetween(jde, TD2UT(p2r, true)) {
if isFiniteFloat(p2r) && !mercuryStationBetween(jde, UTC2TT(p2r)) {
return p2r
}
best := earliestFiniteEventUT(p2r, NextMercuryRetrogradeToPrograde(jde))
@@ -499,7 +499,7 @@ func NextMercuryRetrograde(jde float64) float64 {
}
if motion < -mercuryStationMotionTolerance {
r2p := NextMercuryRetrogradeToPrograde(jde)
if isFiniteFloat(r2p) && !mercuryStationBetween(jde, TD2UT(r2p, true)) {
if isFiniteFloat(r2p) && !mercuryStationBetween(jde, UTC2TT(r2p)) {
return r2p
}
best := earliestFiniteEventUT(NextMercuryProgradeToRetrograde(jde), r2p)
@@ -522,7 +522,7 @@ func LastMercuryRetrograde(jde float64) float64 {
motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay)
if motion > mercuryStationMotionTolerance {
r2p := LastMercuryRetrogradeToPrograde(jde)
if isFiniteFloat(r2p) && !mercuryStationBetween(TD2UT(r2p, true), jde) {
if isFiniteFloat(r2p) && !mercuryStationBetween(UTC2TT(r2p), jde) {
return r2p
}
best := latestFiniteEventUT(LastMercuryProgradeToRetrograde(jde), r2p)
@@ -533,7 +533,7 @@ func LastMercuryRetrograde(jde float64) float64 {
}
if motion < -mercuryStationMotionTolerance {
p2r := LastMercuryProgradeToRetrograde(jde)
if isFiniteFloat(p2r) && !mercuryStationBetween(TD2UT(p2r, true), jde) {
if isFiniteFloat(p2r) && !mercuryStationBetween(UTC2TT(p2r), jde) {
return p2r
}
best := latestFiniteEventUT(p2r, LastMercuryRetrogradeToPrograde(jde))
@@ -572,9 +572,9 @@ func mercurySunElongationN(jde float64, n int) float64 {
// 窗口两端是世界时,目标函数收力学时,因此逐次换算。
func mercuryGreatestElongationInWindow(start, end 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 {
return MercurySunElongation(TD2UT(utJD, true))
return MercurySunElongation(UTC2TT(utJD))
})
}
@@ -13,7 +13,7 @@ func TestMercuryRetrogradeFastPathKeepsTypedCandidateOrder(t *testing.T) {
1644733.927538287,
1645082.416782375,
} {
queryTT := TD2UT(queryUT, true)
queryTT := UTC2TT(queryUT)
nextP2R := NextMercuryProgradeToRetrograde(queryTT)
nextR2P := NextMercuryRetrogradeToPrograde(queryTT)
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 {
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) {
@@ -22,19 +22,19 @@ func TestMercuryTypedStationRegression1929(t *testing.T) {
nextP2R := NextMercuryProgradeToRetrograde(query)
nextR2P := NextMercuryRetrogradeToPrograde(query)
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 {
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)
lastP2R := LastMercuryProgradeToRetrograde(query)
lastR2P := LastMercuryRetrogradeToPrograde(query)
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 {
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"
)
func MoonLo(jd float64) float64 { //'月球平黄经
return planet.MoonLo(jd)
func MoonLo(jde float64) float64 { //'月球平黄经
return planet.MoonLo(jde)
}
func SunMoonAngle(jd float64) float64 { // '月日距角
return planet.SunMoonAngle(jd)
func SunMoonAngle(jde float64) float64 { // '月日距角
return planet.SunMoonAngle(jde)
}
func MoonM(jd float64) float64 { // '月平近点角
return planet.MoonM(jd)
func MoonM(jde float64) float64 { // '月平近点角
return planet.MoonM(jde)
}
func MoonLonX(jd float64) float64 { // As Double '月球经度参数(到升交点的平角距离)
return planet.MoonLonX(jd)
func MoonLonX(jde float64) float64 { // As Double '月球经度参数(到升交点的平角距离)
return planet.MoonLonX(jde)
}
func MoonI(jd float64) float64 {
return planet.MoonI(jd)
func MoonI(jde float64) float64 {
return planet.MoonI(jde)
}
func MoonR(jd float64) float64 {
return planet.MoonR(jd)
func MoonR(jde float64) float64 {
return planet.MoonR(jde)
}
func MoonB(jd float64) float64 {
return planet.MoonB(jd)
func MoonB(jde float64) float64 {
return planet.MoonB(jde)
}
func MoonTrueLo(jd float64) float64 {
return planet.MoonTrueLo(jd)
func MoonTrueLo(jde float64) float64 {
return planet.MoonTrueLo(jde)
}
func MoonTrueBo(jd float64) float64 {
return planet.MoonTrueBo(jd)
func MoonTrueBo(jde float64) float64 {
return planet.MoonTrueBo(jde)
}
func MoonAway(jd float64) float64 { //'月地距离
return planet.MoonAway(jd)
func MoonAway(jde float64) float64 { //'月地距离
return planet.MoonAway(jde)
}
/*
* @name 月球视黄经
*/
func MoonApparentLo(jd float64) float64 {
return MoonTrueLo(jd) + Nutation2000Bi(jd)
func MoonApparentLo(jde float64) float64 {
return MoonTrueLo(jde) + Nutation2000Bi(jde)
}
/*
* 月球真赤纬
*/
func MoonTrueDec(jd float64) float64 {
moonLo := MoonApparentLo(jd)
moonBo := MoonTrueBo(jd)
tmp := Sin(moonBo)*Cos(TrueObliquity(jd)) + Cos(moonBo)*Sin(TrueObliquity(jd))*Sin(moonLo)
func MoonTrueDec(jde float64) float64 {
moonLo := MoonApparentLo(jde)
moonBo := MoonTrueBo(jde)
tmp := Sin(moonBo)*Cos(TrueObliquity(jde)) + Cos(moonBo)*Sin(TrueObliquity(jde))*Sin(moonLo)
res := ArcSin(tmp)
return res
}
@@ -64,31 +64,31 @@ func MoonTrueDec(jd float64) float64 {
/*
* 月球真赤经
*/
func MoonTrueRa(jd float64) float64 {
return LoToRa(jd, MoonApparentLo(jd), MoonTrueBo(jd))
func MoonTrueRa(jde float64) float64 {
return LoToRa(jde, MoonApparentLo(jde), MoonTrueBo(jde))
}
func MoonTrueRaDec(jd float64) (float64, float64) {
return LoBoToRaDec(jd, MoonApparentLo(jd), MoonTrueBo(jd))
func MoonTrueRaDec(jde float64) (float64, float64) {
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.
func MoonApparentRa(jd, lon, lat float64, tz int) float64 {
jde := TD2UT(jd, true)
utcJD := jde - float64(tz)/24.000
ra := MoonTrueRa(utcJD)
dec := MoonTrueDec(utcJD)
away := MoonAway(utcJD) / 149597870.7
// 本地时刻加 ΔT 再减时区偏移即该地时刻的 TT(加法可交换),故 jde 就是 TT。
jde := UTC2TT(jd) - float64(tz)/24.000
ra := MoonTrueRa(jde)
dec := MoonTrueDec(jde)
away := MoonAway(jde) / 149597870.7
topoRA := TopocentricRa(ra, dec, lat, lon, jd-float64(tz)/24.000, away, 0)
return topoRA
}
func MoonApparentDec(jd, lon, lat, tz float64) float64 {
jde := TD2UT(jd, true)
utcJD := jde - tz/24
ra := MoonTrueRa(utcJD)
dec := MoonTrueDec(utcJD)
away := MoonAway(utcJD) / 149597870.7
// 同上:jde 是当地时刻的 TT。
jde := UTC2TT(jd) - tz/24
ra := MoonTrueRa(jde)
dec := MoonTrueDec(jde)
away := MoonAway(jde) / 149597870.7
topoDec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
return topoDec
}
+18 -18
View File
@@ -3,39 +3,39 @@ package basic
import . "b612.me/astro/tools"
// MoonBrightLimbPositionAngle 月亮明亮边缘位置角 / position angle of the Moon's bright limb.
func MoonBrightLimbPositionAngle(jd float64) float64 {
return MoonBrightLimbPositionAngleN(jd, -1)
func MoonBrightLimbPositionAngle(jde float64) float64 {
return MoonBrightLimbPositionAngleN(jde, -1)
}
// MoonBrightLimbPositionAngleN 月亮明亮边缘位置角(截断版) / truncated position angle of the Moon's bright limb.
func MoonBrightLimbPositionAngleN(jd float64, n int) float64 {
sunRA, sunDec := HSunApparentRaDecN(jd, n)
moonRA, moonDec := HMoonTrueRaDecN(jd, n)
func MoonBrightLimbPositionAngleN(jde float64, n int) float64 {
sunRA, sunDec := HSunApparentRaDecN(jde, n)
moonRA, moonDec := HMoonTrueRaDecN(jde, n)
return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec)
}
// MoonTopocentricBrightLimbPositionAngle 月亮站心明亮边缘位置角 / topocentric position angle of the Moon's bright limb.
func MoonTopocentricBrightLimbPositionAngle(jd, observerLon, observerLat, height float64) float64 {
return MoonTopocentricBrightLimbPositionAngleN(jd, observerLon, observerLat, height, -1)
func MoonTopocentricBrightLimbPositionAngle(jde, observerLon, observerLat, height float64) float64 {
return MoonTopocentricBrightLimbPositionAngleN(jde, observerLon, observerLat, height, -1)
}
// MoonTopocentricBrightLimbPositionAngleN 月亮站心明亮边缘位置角(截断版) / truncated topocentric position angle of the Moon's bright limb.
func MoonTopocentricBrightLimbPositionAngleN(jd, observerLon, observerLat, height float64, n int) float64 {
sunRA, sunDec := sunTopocentricApparentRaDecN(jd, observerLon, observerLat, height, n)
moonRA, moonDec := moonTopocentricApparentRaDecN(jd, observerLon, observerLat, height, n)
func MoonTopocentricBrightLimbPositionAngleN(jde, observerLon, observerLat, height float64, n int) float64 {
sunRA, sunDec := sunTopocentricApparentRaDecN(jde, observerLon, observerLat, height, n)
moonRA, moonDec := moonTopocentricApparentRaDecN(jde, observerLon, observerLat, height, n)
return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec)
}
func moonTopocentricApparentRaDecN(jd, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA := HMoonTrueRaN(jd, n)
geocentricDec := HMoonTrueDecN(jd, n)
distanceAU := HMoonAwayN(jd, n) / moonPhysicalAstronomicalUnitKM
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TD2UT(jd, false), distanceAU, height)
func moonTopocentricApparentRaDecN(jde, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA := HMoonTrueRaN(jde, n)
geocentricDec := HMoonTrueDecN(jde, n)
distanceAU := HMoonAwayN(jde, n) / moonPhysicalAstronomicalUnitKM
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TT2UTC(jde), distanceAU, height)
}
func sunTopocentricApparentRaDecN(jd, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA, geocentricDec := HSunApparentRaDecN(jd, n)
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TD2UT(jd, false), EarthAwayN(jd, n), height)
func sunTopocentricApparentRaDecN(jde, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA, geocentricDec := HSunApparentRaDecN(jde, n)
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TT2UTC(jde), EarthAwayN(jde, n), height)
}
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) {
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)
gotN := MoonBrightLimbPositionAngleN(jd, -1)
@@ -21,7 +21,7 @@ func TestMoonBrightLimbPositionAngleNFullMatchesDefault(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)
assertFiniteRange(t, "MoonTopocentricBrightLimbPositionAngle", got, 0, 360, true)
@@ -34,7 +34,7 @@ func TestMoonGeocentricApparentCoordinatesMatchHorizonsBaseline(t *testing.T) {
if err != nil {
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
assertPlanetApparentAngleClose(t, prefix+".RightAscension", HMoonGeocentricApparentRa(jd), sample.RightAscension, 0.001)
@@ -54,7 +54,7 @@ func TestMoonGeocentricTrueCoordinatesFollowDefinition(t *testing.T) {
}
for _, sample := range samples {
jd := TD2UT(Date2JDE(sample.UTC()), true)
jd := UTC2TT(Date2JD(sample.UTC()))
wantRA, wantDec := LoBoToRaDec(jd, HMoonTrueLo(jd), HMoonTrueBo(jd))
gotRA, gotDec := HMoonGeocentricTrueRaDec(jd)
+2 -2
View File
@@ -6,7 +6,7 @@ import (
)
func TestHMoonGeocentricApparentRaDecComponentsMatch(t *testing.T) {
jd := TD2UT(JDECalc(2026, 1, 1.25), true)
jd := UTC2TT(JDCalc(2026, 1, 1.25))
ra, dec := HMoonGeocentricApparentRaDec(jd)
if diff := math.Abs(ra - HMoonGeocentricApparentRa(jd)); diff > 1e-12 {
@@ -18,7 +18,7 @@ func TestHMoonGeocentricApparentRaDecComponentsMatch(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)
if diff := math.Abs(ra - HMoonGeocentricTrueRa(jd)); diff > 1e-12 {
+5 -48
View File
@@ -1,58 +1,15 @@
package basic
import "math"
// MoonHorizon 返回 UT 儒略日下海平面几何月球中心地平圈(月球恰好在地平线上的观测者轨迹,
// MoonHorizon 返回 UTC 儒略日下海平面几何月球中心地平圈(月球恰好在地平线上的观测者轨迹,
// 即月下点周围的地平圈)的 [经度, 纬度] 顶点,单位为度,不重复首点。视差与椭球口径同
// HMoonHeight,不含折射;与 HMoonHeight(经, 纬, ..., 0) 配合时该圈上的点高度角为 0。
// 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
// ellipsoid match HMoonHeight; refraction is excluded.
func MoonHorizon(jdUT float64, samples int) [][2]float64 {
if !finite(jdUT) {
func MoonHorizon(jdUTC float64, samples int) [][2]float64 {
if !finite(jdUTC) {
return nil
}
if samples <= 0 {
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
return MoonStateAt(jdUTC).MoonHorizon(samples)
}
+92 -1
View File
@@ -3,10 +3,11 @@ package basic
import (
"math"
"testing"
"time"
)
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)
if len(points) != 360 {
t.Fatalf("horizon points=%d", len(points))
@@ -21,3 +22,93 @@ func TestMoonHorizonMatchesTopocentricAltitude(t *testing.T) {
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.
type DeclinationEvent struct {
// JDE 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JDE float64
// JD 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JD float64
// Declination 是该时刻月心地心赤纬,单位度 / geocentric lunar declination at the event, in degrees.
Declination float64
}
@@ -124,8 +124,8 @@ func ClosestMoonMaximumSouthDeclination(jd float64) DeclinationEvent {
func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDeclinationCoefficients) []DeclinationEvent {
startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
endUTC := startUTC.AddDate(0, 1, 0)
startTT := TD2UT(Date2JDE(startUTC), true)
endTT := TD2UT(Date2JDE(endUTC), true)
startTT := UTC2TT(Date2JD(startUTC))
endTT := UTC2TT(Date2JD(endUTC))
kStart := int(math.Floor((startTT-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)
for k := kStart; k <= kEnd; k++ {
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) {
continue
}
@@ -150,7 +150,7 @@ func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDe
}
sort.Slice(events, func(i, j int) bool {
return events[i].JDE < events[j].JDE
return events[i].JD < events[j].JD
})
return events
}
@@ -161,7 +161,7 @@ func moonMaximumDeclinationEvent(k int, coeffs moonMaxDeclinationCoefficients, c
return HMoonTrueDecN(sampleTT, -1)
})
return DeclinationEvent{
JDE: TD2UT(eventTT, false),
JD: TT2UTC(eventTT),
Declination: declination,
}
}
@@ -180,10 +180,10 @@ func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficie
event := moonMaximumDeclinationEvent(centerK, coeffs, cfg)
// 事件时刻随周期序号单调递增:该方向上最近的候选就是 centerK 沿该方向第一个满足方向不变量
// 的周期;命中后只需再回退检查更早的周期是否同样满足,可达范围仍旧是 ±3 个周期。
if !moonMaximumDeclinationMatchesDirection(event.JDE-jd, direction, includeCurrent) {
if !moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
event = moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg)
if moonMaximumDeclinationMatchesDirection(event.JDE-jd, direction, includeCurrent) {
if moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
return event
}
}
@@ -191,7 +191,7 @@ func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficie
}
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
previous := moonMaximumDeclinationEvent(centerK-step*offset, coeffs, cfg)
if !moonMaximumDeclinationMatchesDirection(previous.JDE-jd, direction, includeCurrent) {
if !moonMaximumDeclinationMatchesDirection(previous.JD-jd, direction, includeCurrent) {
break
}
event = previous
@@ -207,15 +207,15 @@ func moonClosestMaximumDeclination(jd float64, coeffs moonMaxDeclinationCoeffici
centerK := moonMaximumDeclinationOffset(jd, coeffs)
center := moonMaximumDeclinationEvent(centerK, coeffs, cfg)
var last, next DeclinationEvent
if center.JDE <= jd {
if center.JD <= jd {
last = center
next = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, 1, false)
} else {
next = center
last = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, -1, true)
}
lastDistance := math.Abs(jd - last.JDE)
nextDistance := math.Abs(next.JDE - jd)
lastDistance := math.Abs(jd - last.JD)
nextDistance := math.Abs(next.JD - jd)
if lastDistance <= nextDistance {
return last
}
@@ -231,7 +231,7 @@ func moonMaximumDeclinationAround(jd float64, coeffs moonMaxDeclinationCoefficie
}
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
event := moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg)
if moonMaximumDeclinationMatchesDirection(event.JDE-jd, direction, includeCurrent) {
if moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
return event
}
}
@@ -251,7 +251,7 @@ func moonMaximumDeclinationSearchConfig(coeffs moonMaxDeclinationCoefficients) a
// moonMaximumDeclinationOffset 查询时刻落在哪个平均周期(种子多项式的中心序号)。
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 {
@@ -272,7 +272,7 @@ func moonMaximumDeclinationMatchesDirection(delta float64, direction int, includ
}
func moonMaximumDeclinationEarlier(a, b DeclinationEvent) bool {
return a.JDE < b.JDE
return a.JD < b.JD
}
func moonMaximumDeclinationSeedTT(k int, coeffs moonMaxDeclinationCoefficients) float64 {
+5 -5
View File
@@ -18,7 +18,7 @@ func moonMaximumDeclinationReferenceSearch(jd float64, coeffs moonMaxDeclination
var best DeclinationEvent
for offset := -moonMaxDeclinationSearchSpan; offset <= moonMaxDeclinationSearchSpan; offset++ {
event := moonMaximumDeclinationEvent(centerK+offset, coeffs, cfg)
delta := event.JDE - jd
delta := event.JD - jd
if !moonMaximumDeclinationMatchesDirection(delta, direction, includeCurrent) {
continue
}
@@ -38,8 +38,8 @@ func moonMaximumDeclinationReferenceClosest(jd float64, coeffs moonMaxDeclinatio
}
last := moonMaximumDeclinationReferenceSearch(jd, coeffs, -1, true)
next := moonMaximumDeclinationReferenceSearch(jd, coeffs, 1, false)
lastDistance := math.Abs(jd - last.JDE)
nextDistance := math.Abs(next.JDE - jd)
lastDistance := math.Abs(jd - last.JD)
nextDistance := math.Abs(next.JD - jd)
if lastDistance <= nextDistance {
return last
}
@@ -48,7 +48,7 @@ func moonMaximumDeclinationReferenceClosest(jd float64, coeffs moonMaxDeclinatio
func TestMoonMaximumDeclinationPruningMatchesFullSearch(t *testing.T) {
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 {
name string
dir int
@@ -77,7 +77,7 @@ func TestMoonMaximumDeclinationPruningMatchesFullSearch(t *testing.T) {
}
func BenchmarkMoonMaximumDeclinationFullSearchReference(b *testing.B) {
jd := JDECalc(2025, 3, 1)
jd := JDCalc(2025, 3, 1)
b.Run("Next", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = 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)
}
gotTime := JDE2DateByZone(got.JDE, time.UTC, false)
gotTime := JD2DateByZone(got.JD, time.UTC, false)
timeDiff := gotTime.Sub(wantTime)
if timeDiff < 0 {
timeDiff = -timeDiff
@@ -124,23 +124,23 @@ func TestMoonMaximumDeclinationSignsAndOrder(t *testing.T) {
if event.Declination <= 0 {
t.Fatalf("north event #%d should be positive, got %.8f", i+1, event.Declination)
}
if i > 0 && !(north[i-1].JDE < event.JDE) {
t.Fatalf("north events not strictly increasing: %.12f then %.12f", 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].JD, event.JD)
}
}
for i, event := range south {
if event.Declination >= 0 {
t.Fatalf("south event #%d should be negative, got %.8f", i+1, event.Declination)
}
if i > 0 && !(south[i-1].JDE < event.JDE) {
t.Fatalf("south events not strictly increasing: %.12f then %.12f", 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].JD, event.JD)
}
}
}
func TestMoonMaximumDeclinationSearchMatchesMonthlyEvents(t *testing.T) {
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(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.February)...)
assertSameDeclinationEvent(t, "last north", LastMoonMaximumNorthDeclination(queryJDE), expectedDirectionalDeclinationEvent(northEvents, queryJDE, -1, true))
assertSameDeclinationEvent(t, "next north", NextMoonMaximumNorthDeclination(queryJDE), expectedDirectionalDeclinationEvent(northEvents, queryJDE, 1, false))
assertSameDeclinationEvent(t, "closest north", ClosestMoonMaximumNorthDeclination(queryJDE), expectedClosestDeclinationEvent(northEvents, queryJDE))
assertSameDeclinationEvent(t, "last north", LastMoonMaximumNorthDeclination(queryJD), expectedDirectionalDeclinationEvent(northEvents, queryJD, -1, true))
assertSameDeclinationEvent(t, "next north", NextMoonMaximumNorthDeclination(queryJD), expectedDirectionalDeclinationEvent(northEvents, queryJD, 1, false))
assertSameDeclinationEvent(t, "closest north", ClosestMoonMaximumNorthDeclination(queryJD), expectedClosestDeclinationEvent(northEvents, queryJD))
assertSameDeclinationEvent(t, "last south", LastMoonMaximumSouthDeclination(queryJDE), expectedDirectionalDeclinationEvent(southEvents, queryJDE, -1, true))
assertSameDeclinationEvent(t, "next south", NextMoonMaximumSouthDeclination(queryJDE), expectedDirectionalDeclinationEvent(southEvents, queryJDE, 1, false))
assertSameDeclinationEvent(t, "closest south", ClosestMoonMaximumSouthDeclination(queryJDE), expectedClosestDeclinationEvent(southEvents, queryJDE))
assertSameDeclinationEvent(t, "last south", LastMoonMaximumSouthDeclination(queryJD), expectedDirectionalDeclinationEvent(southEvents, queryJD, -1, true))
assertSameDeclinationEvent(t, "next south", NextMoonMaximumSouthDeclination(queryJD), expectedDirectionalDeclinationEvent(southEvents, queryJD, 1, false))
assertSameDeclinationEvent(t, "closest south", ClosestMoonMaximumSouthDeclination(queryJD), expectedClosestDeclinationEvent(southEvents, queryJD))
}
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))
}
exactJDE := north[0].JDE
assertSameDeclinationEvent(t, "exact last north", LastMoonMaximumNorthDeclination(exactJDE), north[0])
assertSameDeclinationEvent(t, "exact closest north", ClosestMoonMaximumNorthDeclination(exactJDE), north[0])
assertSameDeclinationEvent(t, "exact next north", NextMoonMaximumNorthDeclination(exactJDE), north[1])
exactJD := north[0].JD
assertSameDeclinationEvent(t, "exact last north", LastMoonMaximumNorthDeclination(exactJD), north[0])
assertSameDeclinationEvent(t, "exact closest north", ClosestMoonMaximumNorthDeclination(exactJD), north[0])
assertSameDeclinationEvent(t, "exact next north", NextMoonMaximumNorthDeclination(exactJD), north[1])
}
func assertSameDeclinationEvent(t *testing.T, name string, got, want DeclinationEvent) {
t.Helper()
if math.Abs(got.JDE-want.JDE) > 1e-12 {
t.Fatalf("%s JDE mismatch: got %.12f want %.12f", name, got.JDE, want.JDE)
if math.Abs(got.JD-want.JD) > 1e-12 {
t.Fatalf("%s JD mismatch: got %.12f want %.12f", name, got.JD, want.JD)
}
if math.Float64bits(got.Declination) != math.Float64bits(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 (
found bool
best DeclinationEvent
)
for _, event := range events {
delta := event.JDE - queryJDE
delta := event.JD - queryJD
if !moonMaximumDeclinationMatchesDirection(delta, direction, includeCurrent) {
continue
}
@@ -196,17 +196,17 @@ func expectedDirectionalDeclinationEvent(events []DeclinationEvent, queryJDE flo
found = true
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
}
}
return best
}
func expectedClosestDeclinationEvent(events []DeclinationEvent, queryJDE float64) DeclinationEvent {
last := expectedDirectionalDeclinationEvent(events, queryJDE, -1, true)
next := expectedDirectionalDeclinationEvent(events, queryJDE, 1, false)
if math.Abs(queryJDE-last.JDE) <= math.Abs(next.JDE-queryJDE) {
func expectedClosestDeclinationEvent(events []DeclinationEvent, queryJD float64) DeclinationEvent {
last := expectedDirectionalDeclinationEvent(events, queryJD, -1, true)
next := expectedDirectionalDeclinationEvent(events, queryJD, 1, false)
if math.Abs(queryJD-last.JD) <= math.Abs(next.JD-queryJD) {
return last
}
return next
+122 -34
View File
@@ -12,14 +12,14 @@ import (
func MoonAzimuth(jd, lon, lat, tz float64) float64 {
//tmp := (tz*15 - lon) * 4 / 60
calcjd := TD2UT(jd-tz/24, true)
ra := MoonTrueRa(calcjd)
dec := MoonTrueDec(calcjd)
away := MoonAway(calcjd) / 149597870.7
jde := UTC2TT(jd - tz/24)
ra := MoonTrueRa(jde)
dec := MoonTrueDec(jde)
away := MoonAway(jde) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
calcjd = jd - tz/24
st := Limit360(ApparentSiderealTime(calcjd)*15 + lon)
jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*15 + lon)
hourAngle := Limit360(st - nra)
tmp2 := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(ndec)*Cos(lat))
azimuth := ArcTan(tmp2)
@@ -42,14 +42,14 @@ func MoonAzimuth(jd, lon, lat, tz float64) float64 {
func MoonHeight(jd, lon, lat, tz float64) float64 {
// tmp := (tz*15 - lon) * 4 / 60
//truejd=jd-tmp/24;
calcjd := TD2UT(jd-tz/24, true)
ra := MoonTrueRa(calcjd)
dec := MoonTrueDec(calcjd)
away := MoonAway(calcjd) / 149597870.7
jde := UTC2TT(jd - tz/24)
ra := MoonTrueRa(jde)
dec := MoonTrueDec(jde)
away := MoonAway(jde) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
calcjd = jd - tz/24
st := Limit360(ApparentSiderealTime(calcjd)*15 + lon)
jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*15 + lon)
hourAngle := Limit360(st - nra)
tmp2 := Sin(lat)*Sin(ndec) + Cos(ndec)*Cos(lat)*Cos(hourAngle)
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 {
calcjd := TD2UT(jd-tz/24, true)
ra := HMoonTrueRaN(calcjd, n)
dec := HMoonTrueDecN(calcjd, n)
away := HMoonAwayN(calcjd, n) / 149597870.7
jde := UTC2TT(jd - tz/24)
ra := HMoonTrueRaN(jde, n)
dec := HMoonTrueDecN(jde, n)
away := HMoonAwayN(jde, n) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
calcjd = jd - tz/24
st := Limit360(ApparentSiderealTime(calcjd)*15 + lon)
jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*15 + lon)
hourAngle := Limit360(st - nra)
tmp2 := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(ndec)*Cos(lat))
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 {
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 {
calculationJD := TD2UT(jd-tz/24, true)
ra, dec := HMoonTrueRaDecN(calculationJD, n)
distanceKM := HMoonAwayN(calculationJD, n)
calculationJDE := UTC2TT(jd - tz/24)
ra, dec := HMoonTrueRaDecN(calculationJDE, n)
distanceKM := HMoonAwayN(calculationJDE, n)
distanceAU := distanceKM / angularDiameterAstronomicalUnitKM
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)
altitudeSine := Sin(lat)*Sin(topocentricDec) + Cos(topocentricDec)*Cos(lat)*Cos(hourAngle)
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
}
// 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 {
state := hMoonObservationStateN(jd, longitude, latitude, timeZone, height, n)
// 相对观测者下沉地平线的视上缘高度角 / 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
}
func MoonCulminationTime(jde, lon, lat, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5
estimateJD := jde + Limit360(360-MoonTimeAngle(jde, lon, lat, timezone))/15.0/24.0/0.9
limitHA := func(jde, lon, timezone float64) float64 {
ha := MoonTimeAngle(jde, lon, lat, timezone)
func MoonCulminationTime(localJD, lon, lat, timezone float64) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时;ra/dec 为瞬时天球坐标,非 J2000 等固定历元。
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-MoonTimeAngle(localJD, lon, lat, timezone))/15.0/24.0/0.9
limitHA := func(localJD, lon, timezone float64) float64 {
ha := MoonTimeAngle(localJD, lon, lat, timezone)
if ha < 180 {
ha += 360
}
@@ -182,7 +272,7 @@ func MoonCulminationTime(jde, lon, lat, timezone 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)
if timeangle < 0 {
timeangle += 360
@@ -198,8 +288,7 @@ func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, heig
timeZone = longitude / 15
var timeToMeridian float64
civilDayStart := math.Floor(julianDay) + 0.5
//julianDay = math.Floor(julianDay) + 0.5 - originalTimeZone/24 + timeZone/24 // 求0时JDE
//fix:这里时间分界线应当以传入的时区为准,不应当使用当地时区,否则在0时的判断会出错
// 时间分界线以传入的时区为准,不用当地时区,否则 0 时的判断会出错。
julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1)
@@ -282,8 +371,7 @@ func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, heigh
timeZone = longitude / 15
var timeToMeridian float64
civilDayStart := math.Floor(julianDay) + 0.5
//julianDay = math.Floor(julianDay) + 0.5 - originalTimeZone/24 + timeZone/24 // 求0时JDE
//fix:这里时间分界线应当以传入的时区为准,不应当使用当地时区,否则在0时的判断会出错
// 时间分界线以传入的时区为准,不用当地时区,否则 0 时的判断会出错。
julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1)
+6 -8
View File
@@ -6,13 +6,13 @@ import (
. "b612.me/astro/tools"
)
func MoonPhase(jd float64) float64 {
moonBo := HMoonTrueBo(jd)
sunLo := HSunApparentLo(jd)
moonLo := HMoonApparentLo(jd)
func MoonPhase(jde float64) float64 {
moonBo := HMoonTrueBo(jde)
sunLo := HSunApparentLo(jde)
moonLo := HMoonApparentLo(jde)
tmp := Cos(moonBo) * Cos(sunLo-moonLo)
earthSunDistance := Distance(jd) * 149597870.691
i := earthSunDistance * Sin(ArcCos(tmp)) / (HMoonAway(jd) - earthSunDistance*tmp)
earthSunDistance := Distance(jde) * 149597870.691
i := earthSunDistance * Sin(ArcCos(tmp)) / (HMoonAway(jde) - earthSunDistance*tmp)
i = ArcTan(i)
if i < 0 {
i += 180
@@ -197,8 +197,6 @@ func CalcMoonX(year float64, quarterType int) float64 {
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 /= 1000000
//die(tmp2);
//die(JDE." ".tmp." ".tmp2." ".W);
jde = jde + planetaryCorrection + correction
if quarterType == 0 {
jde += W
+24 -24
View File
@@ -28,35 +28,35 @@ type MoonPhysicalInfo struct {
}
// MoonPhysical 月球物理观测参数 / physical observing parameters of the Moon.
func MoonPhysical(jd float64) MoonPhysicalInfo {
return MoonPhysicalN(jd, -1)
func MoonPhysical(jde float64) MoonPhysicalInfo {
return MoonPhysicalN(jde, -1)
}
// MoonPhysicalN 月球物理观测参数(截断版) / truncated physical observing parameters of the Moon.
func MoonPhysicalN(jd float64, n int) MoonPhysicalInfo {
return moonPhysicalNFromCoordinates(jd, n, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n), HMoonTrueRaN(jd, n))
func MoonPhysicalN(jde float64, n int) MoonPhysicalInfo {
return moonPhysicalNFromCoordinates(jde, n, HMoonApparentLoN(jde, n), HMoonTrueBoN(jde, n), HMoonTrueRaN(jde, n))
}
// MoonTopocentricPhysical 月球站心物理观测参数 / topocentric physical observing parameters of the Moon.
func MoonTopocentricPhysical(jd, observerLon, observerLat, height float64) MoonPhysicalInfo {
return MoonTopocentricPhysicalN(jd, observerLon, observerLat, height, -1)
func MoonTopocentricPhysical(jde, observerLon, observerLat, height float64) MoonPhysicalInfo {
return MoonTopocentricPhysicalN(jde, observerLon, observerLat, height, -1)
}
// MoonTopocentricPhysicalN 月球站心物理观测参数(截断版) / truncated topocentric physical observing parameters of the Moon.
func MoonTopocentricPhysicalN(jd, observerLon, observerLat, height float64, n int) MoonPhysicalInfo {
lambda, beta, alpha := moonTopocentricPhysicalCoordinatesN(jd, observerLon, observerLat, height, n)
return moonPhysicalNFromCoordinates(jd, n, lambda, beta, alpha)
func MoonTopocentricPhysicalN(jde, observerLon, observerLat, height float64, n int) MoonPhysicalInfo {
lambda, beta, alpha := moonTopocentricPhysicalCoordinatesN(jde, observerLon, observerLat, height, n)
return moonPhysicalNFromCoordinates(jde, n, lambda, beta, alpha)
}
func moonPhysicalNFromCoordinates(jd float64, n int, lambda, beta, alpha float64) MoonPhysicalInfo {
t := (jd - 2451545.0) / 36525.0
epsilon := TrueObliquity(jd)
deltaPsi := Nutation2000Bi(jd)
func moonPhysicalNFromCoordinates(jde float64, n int, lambda, beta, alpha float64) MoonPhysicalInfo {
t := (jde - 2451545.0) / 36525.0
epsilon := TrueObliquity(jde)
deltaPsi := Nutation2000Bi(jde)
D := Limit360(SunMoonAngle(jd))
sunMeanAnomaly := Limit360(SunM(jd))
moonMeanAnomaly := Limit360(MoonM(jd))
F := Limit360(MoonLonX(jd))
D := Limit360(SunMoonAngle(jde))
sunMeanAnomaly := Limit360(SunM(jde))
moonMeanAnomaly := Limit360(MoonM(jde))
F := Limit360(MoonLonX(jde))
omega := moonPhysicalMeanAscendingNode(t)
E := 1 - 0.002516*t - 0.0000074*t*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) {
geocentricRA := HMoonTrueRaN(jd, n)
geocentricDec := HMoonTrueDecN(jd, n)
distanceAU := HMoonAwayN(jd, n) / moonPhysicalAstronomicalUnitKM
utJD := TD2UT(jd, false)
func moonTopocentricPhysicalCoordinatesN(jde, observerLon, observerLat, height float64, n int) (lambda, beta, alpha float64) {
geocentricRA := HMoonTrueRaN(jde, n)
geocentricDec := HMoonTrueDecN(jde, n)
distanceAU := HMoonAwayN(jde, n) / moonPhysicalAstronomicalUnitKM
utcJD := TT2UTC(jde)
var topocentricDec float64
alpha, topocentricDec = TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, utJD, distanceAU, height)
lambda, beta = RaDecToLoBo(jd, alpha, topocentricDec)
alpha, topocentricDec = TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, utcJD, distanceAU, height)
lambda, beta = RaDecToLoBo(jde, alpha, topocentricDec)
return
}
+1 -1
View File
@@ -43,7 +43,7 @@ func TestMoonPhysicalSampleSweepFiniteAndInRange(t *testing.T) {
}
for _, date := range dates {
jd := TD2UT(Date2JDE(date.UTC()), true)
jd := UTC2TT(Date2JD(date.UTC()))
info := MoonPhysical(jd)
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 {
return math.NaN()
}
return TD2UT(eventTT, false)
return TT2UTC(eventTT)
}
func moonPlanetConjunctionCollectLocalEvent(result *moonPlanetConjunctionLocalResult, queryTT, eventUT float64) {
+33 -33
View File
@@ -92,9 +92,9 @@ func TestMoonPlanetConjunctionsMatchHorizonsBaseline(t *testing.T) {
if err != nil {
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)
gotTime := JDE2DateByZone(gotUT, time.UTC, false)
gotTime := JD2DateByZone(gotUT, time.UTC, false)
diff := gotTime.Sub(wantTime)
if diff < 0 {
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)
}
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), tc.planet, -1))
delta := math.Abs(moonPlanetConjunctionDeltaAt(UTC2TT(gotUT), tc.planet, -1))
if delta > 0.01 {
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 {
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)
eventTime := JDE2DateByZone(eventUT, time.UTC, false)
queryAtTT := TD2UT(Date2JDE(eventTime.UTC()), true)
queryAfterTT := TD2UT(Date2JDE(eventTime.Add(time.Hour).UTC()), true)
eventTime := JD2DateByZone(eventUT, time.UTC, false)
queryAtTT := UTC2TT(Date2JD(eventTime.UTC()))
queryAfterTT := UTC2TT(Date2JD(eventTime.Add(time.Hour).UTC()))
exactNext := NextMoonPlanetConjunction(queryAtTT, planet)
exactClosest := ClosestMoonPlanetConjunction(queryAtTT, planet)
@@ -159,7 +159,7 @@ func TestMoonPlanetConjunctionDirectionalConsistencyAtComputedEvent(t *testing.T
"exactClosest": exactClosest,
"lastAfterEvent": exactLastAfter,
} {
gotTime := JDE2DateByZone(gotUT, time.UTC, false)
gotTime := JD2DateByZone(gotUT, time.UTC, false)
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)
}
@@ -169,25 +169,25 @@ func TestMoonPlanetConjunctionDirectionalConsistencyAtComputedEvent(t *testing.T
func TestMoonPlanetConjunctionRejectsOppositionBranchJump(t *testing.T) {
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)
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
if math.Abs(lastUT-Date2JDE(query)) <= 5.0/86400.0 {
t.Fatalf("last returned query time on branch jump: got %s", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
if math.Abs(lastUT-Date2JD(query)) <= 5.0/86400.0 {
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 {
t.Fatalf("next returned query time on branch jump: got %s", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano))
if math.Abs(nextUT-Date2JD(query)) <= 5.0/86400.0 {
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{
"last": lastUT,
"next": nextUT,
} {
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), MoonPlanetConjunctionSaturn, -1))
delta := math.Abs(moonPlanetConjunctionDeltaAt(UTC2TT(gotUT), MoonPlanetConjunctionSaturn, -1))
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 {
queryTT := TD2UT(Date2JDE(sample.query.UTC()), true)
queryTT := UTC2TT(Date2JD(sample.query.UTC()))
lastUT := LastMoonPlanetConjunction(queryTT, sample.planet)
nextUT := NextMoonPlanetConjunction(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)
}
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) {
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) {
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{
"last": lastUT,
"next": nextUT,
"closest": closestUT,
} {
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), sample.planet, -1))
delta := math.Abs(moonPlanetConjunctionDeltaAt(UTC2TT(gotUT), sample.planet, -1))
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) {
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)
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)
const tolerance = 5.0 / 86400.0
if diff := math.Abs(lastUT - Date2JDE(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)
if diff := math.Abs(lastUT - Date2JD(wantLast)); diff > tolerance {
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 {
t.Fatalf("next mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), wantNext.Format(time.RFC3339Nano), diff*86400)
if diff := math.Abs(nextUT - Date2JD(wantNext)); diff > tolerance {
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) {
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) {
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)
query := JDE2DateByZone(eventUT, time.UTC, false).Add(time.Second)
queryTT := TD2UT(Date2JDE(query.UTC()), true)
query := JD2DateByZone(eventUT, time.UTC, false).Add(time.Second)
queryTT := UTC2TT(Date2JD(query.UTC()))
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionMercury)
if eventUTQueryTTDelta(nextUT, queryTT) <= 0 {
t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%.6fs",
query.Format(time.RFC3339Nano),
JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
JD2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
eventUTQueryTTDelta(nextUT, queryTT)*86400,
)
}
if sameEventJD(nextUT, eventUT) {
t.Fatalf("next conjunction should advance to a later event: event=%s next=%s",
JDE2DateByZone(eventUT, time.UTC, false).Format(time.RFC3339Nano),
JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
JD2DateByZone(eventUT, 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
}
func HMoonTrueLo(jd float64) float64 { //计算月亮
return HMoonTrueLoN(jd, -1)
func HMoonTrueLo(jde float64) float64 { //计算月亮
return HMoonTrueLoN(jde, -1)
}
func HMoonTrueLoN(jd float64, n int) float64 { //计算月亮
@@ -143,8 +143,8 @@ func HMoonTrueLoN(jd float64, n int) float64 { //计算月亮
return Limit360(v)
}
func HMoonTrueBo(jd float64) float64 {
return HMoonTrueBoN(jd, -1)
func HMoonTrueBo(jde float64) float64 {
return HMoonTrueBoN(jde, -1)
}
func HMoonTrueBoN(jd float64, n int) float64 {
@@ -152,8 +152,8 @@ func HMoonTrueBoN(jd float64, n int) float64 {
return v
}
func HMoonAway(jd float64) float64 { //'月地距离
return HMoonAwayN(jd, -1)
func HMoonAway(jde float64) float64 { //'月地距离
return HMoonAwayN(jde, -1)
}
func HMoonAwayN(jd float64, n int) float64 { //'月地距离
@@ -163,95 +163,95 @@ func HMoonAwayN(jd float64, n int) float64 { //'月地距离
/*
* @name 月球视黄经
*/
func HMoonApparentLo(jd float64) float64 {
return HMoonApparentLoN(jd, -1)
func HMoonApparentLo(jde float64) float64 {
return HMoonApparentLoN(jde, -1)
}
func HMoonApparentLoN(jd float64, n int) float64 {
return HMoonTrueLoN(jd, n) + Nutation2000Bi(jd)
func HMoonApparentLoN(jde float64, n int) float64 {
return HMoonTrueLoN(jde, n) + Nutation2000Bi(jde)
}
// HMoonGeocentricApparentRa 月亮地心视赤经 / apparent geocentric right ascension of the Moon.
func HMoonGeocentricApparentRa(jd float64) float64 {
return HMoonGeocentricApparentRaN(jd, -1)
func HMoonGeocentricApparentRa(jde float64) float64 {
return HMoonGeocentricApparentRaN(jde, -1)
}
// HMoonGeocentricApparentRaN 月亮地心视赤经(截断版) / truncated apparent geocentric right ascension of the Moon.
func HMoonGeocentricApparentRaN(jd float64, n int) float64 {
return LoToRa(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n))
func HMoonGeocentricApparentRaN(jde float64, n int) float64 {
return LoToRa(jde, HMoonApparentLoN(jde, n), HMoonTrueBoN(jde, n))
}
// HMoonGeocentricApparentDec 月亮地心视赤纬 / apparent geocentric declination of the Moon.
func HMoonGeocentricApparentDec(jd float64) float64 {
return HMoonGeocentricApparentDecN(jd, -1)
func HMoonGeocentricApparentDec(jde float64) float64 {
return HMoonGeocentricApparentDecN(jde, -1)
}
// HMoonGeocentricApparentDecN 月亮地心视赤纬(截断版) / truncated apparent geocentric declination of the Moon.
func HMoonGeocentricApparentDecN(jd float64, n int) float64 {
return ArcSin(Sin(HMoonTrueBoN(jd, n))*Cos(TrueObliquity(jd)) +
Cos(HMoonTrueBoN(jd, n))*Sin(TrueObliquity(jd))*Sin(HMoonApparentLoN(jd, n)))
func HMoonGeocentricApparentDecN(jde float64, n int) float64 {
return ArcSin(Sin(HMoonTrueBoN(jde, n))*Cos(TrueObliquity(jde)) +
Cos(HMoonTrueBoN(jde, n))*Sin(TrueObliquity(jde))*Sin(HMoonApparentLoN(jde, n)))
}
// HMoonGeocentricApparentRaDec 月亮地心视赤经、视赤纬 / apparent geocentric right ascension and declination of the Moon.
func HMoonGeocentricApparentRaDec(jd float64) (float64, float64) {
return HMoonGeocentricApparentRaDecN(jd, -1)
func HMoonGeocentricApparentRaDec(jde float64) (float64, float64) {
return HMoonGeocentricApparentRaDecN(jde, -1)
}
// HMoonGeocentricApparentRaDecN 月亮地心视赤经、视赤纬(截断版) / truncated apparent geocentric right ascension and declination of the Moon.
func HMoonGeocentricApparentRaDecN(jd float64, n int) (float64, float64) {
return LoBoToRaDec(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n))
func HMoonGeocentricApparentRaDecN(jde float64, n int) (float64, float64) {
return LoBoToRaDec(jde, HMoonApparentLoN(jde, n), HMoonTrueBoN(jde, n))
}
// HMoonGeocentricTrueRa 月亮地心真赤经 / true geocentric right ascension of the Moon.
func HMoonGeocentricTrueRa(jd float64) float64 {
return HMoonGeocentricTrueRaN(jd, -1)
func HMoonGeocentricTrueRa(jde float64) float64 {
return HMoonGeocentricTrueRaN(jde, -1)
}
// HMoonGeocentricTrueRaN 月亮地心真赤经(截断版) / truncated true geocentric right ascension of the Moon.
func HMoonGeocentricTrueRaN(jd float64, n int) float64 {
return LoToRa(jd, HMoonTrueLoN(jd, n), HMoonTrueBoN(jd, n))
func HMoonGeocentricTrueRaN(jde float64, n int) float64 {
return LoToRa(jde, HMoonTrueLoN(jde, n), HMoonTrueBoN(jde, n))
}
// HMoonGeocentricTrueDec 月亮地心真赤纬 / true geocentric declination of the Moon.
func HMoonGeocentricTrueDec(jd float64) float64 {
return HMoonGeocentricTrueDecN(jd, -1)
func HMoonGeocentricTrueDec(jde float64) float64 {
return HMoonGeocentricTrueDecN(jde, -1)
}
// HMoonGeocentricTrueDecN 月亮地心真赤纬(截断版) / truncated true geocentric declination of the Moon.
func HMoonGeocentricTrueDecN(jd float64, n int) float64 {
return ArcSin(Sin(HMoonTrueBoN(jd, n))*Cos(TrueObliquity(jd)) +
Cos(HMoonTrueBoN(jd, n))*Sin(TrueObliquity(jd))*Sin(HMoonTrueLoN(jd, n)))
func HMoonGeocentricTrueDecN(jde float64, n int) float64 {
return ArcSin(Sin(HMoonTrueBoN(jde, n))*Cos(TrueObliquity(jde)) +
Cos(HMoonTrueBoN(jde, n))*Sin(TrueObliquity(jde))*Sin(HMoonTrueLoN(jde, n)))
}
// HMoonGeocentricTrueRaDec 月亮地心真赤经、真赤纬 / true geocentric right ascension and declination of the Moon.
func HMoonGeocentricTrueRaDec(jd float64) (float64, float64) {
return HMoonGeocentricTrueRaDecN(jd, -1)
func HMoonGeocentricTrueRaDec(jde float64) (float64, float64) {
return HMoonGeocentricTrueRaDecN(jde, -1)
}
// HMoonGeocentricTrueRaDecN 月亮地心真赤经、真赤纬(截断版) / truncated true geocentric right ascension and declination of the Moon.
func HMoonGeocentricTrueRaDecN(jd float64, n int) (float64, float64) {
return LoBoToRaDec(jd, HMoonTrueLoN(jd, n), HMoonTrueBoN(jd, n))
func HMoonGeocentricTrueRaDecN(jde float64, n int) (float64, float64) {
return LoBoToRaDec(jde, HMoonTrueLoN(jde, n), HMoonTrueBoN(jde, n))
}
func HMoonTrueRaDec(jd float64) (float64, float64) {
return HMoonTrueRaDecN(jd, -1)
func HMoonTrueRaDec(jde float64) (float64, float64) {
return HMoonTrueRaDecN(jde, -1)
}
func HMoonTrueRaDecN(jd float64, n int) (float64, float64) {
return LoBoToRaDec(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n))
func HMoonTrueRaDecN(jde float64, n int) (float64, float64) {
return LoBoToRaDec(jde, HMoonApparentLoN(jde, n), HMoonTrueBoN(jde, n))
}
/*
* 月球真赤纬
*/
func HMoonTrueDec(jd float64) float64 {
return HMoonTrueDecN(jd, -1)
func HMoonTrueDec(jde float64) float64 {
return HMoonTrueDecN(jde, -1)
}
func HMoonTrueDecN(jd float64, n int) float64 {
moonLo := HMoonApparentLoN(jd, n)
moonBo := HMoonTrueBoN(jd, n)
tmp := Sin(moonBo)*Cos(TrueObliquity(jd)) + Cos(moonBo)*Sin(TrueObliquity(jd))*Sin(moonLo)
func HMoonTrueDecN(jde float64, n int) float64 {
moonLo := HMoonApparentLoN(jde, n)
moonBo := HMoonTrueBoN(jde, n)
tmp := Sin(moonBo)*Cos(TrueObliquity(jde)) + Cos(moonBo)*Sin(TrueObliquity(jde))*Sin(moonLo)
res := ArcSin(tmp)
return res
}
@@ -259,12 +259,12 @@ func HMoonTrueDecN(jd float64, n int) float64 {
/*
* 月球真赤经
*/
func HMoonTrueRa(jd float64) float64 {
return HMoonTrueRaN(jd, -1)
func HMoonTrueRa(jde float64) float64 {
return HMoonTrueRaN(jde, -1)
}
func HMoonTrueRaN(jd float64, n int) float64 {
return LoToRa(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n))
func HMoonTrueRaN(jde float64, n int) float64 {
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) {
jde := TD2UT(jd, true)
jde := UTC2TT(jd)
ra := HMoonTrueRaN(jde-tz/24, n)
dec := HMoonTrueDecN(jde-tz/24, n)
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 {
jde := TD2UT(jd, true)
ra := HMoonTrueRaN(jde-tz/24, n)
dec := HMoonTrueDecN(jde-tz/24, n)
away := HMoonAwayN(jde-tz/24, n) / 149597870.7
jde := UTC2TT(jd) - tz/24
ra := HMoonTrueRaN(jde, n)
dec := HMoonTrueDecN(jde, n)
away := HMoonAwayN(jde, n) / 149597870.7
topoRA := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
return topoRA
}
@@ -300,10 +300,10 @@ func HMoonApparentDec(jd, lon, lat, tz float64) float64 {
}
func HMoonApparentDecN(jd, lon, lat, tz float64, n int) float64 {
jde := TD2UT(jd, true)
ra := HMoonTrueRaN(jde-tz/24, n)
dec := HMoonTrueDecN(jde-tz/24, n)
away := HMoonAwayN(jde-tz/24, n) / 149597870.7
jde := UTC2TT(jd) - tz/24
ra := HMoonTrueRaN(jde, n)
dec := HMoonTrueDecN(jde, n)
away := HMoonAwayN(jde, n) / 149597870.7
topoDec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
return topoDec
}
+6 -6
View File
@@ -39,10 +39,10 @@ func TestMoonRiseSetMissingEventConvention(t *testing.T) {
lon, lat, tz float64
riseErr, setErr error
}{
{"极昼:全天在地平线上", JDECalc(2023, 6, 21), 0, 85, 0, ErrNeverSet, ErrNeverSet},
{"极夜:全天在地平线下", JDECalc(2023, 12, 22), 0, -85, 0, ErrNeverRise, ErrNeverRise},
{"当日无升起但别日有", JDECalc(2024, 2, 29), 0, 60, 0, ErrNotOnThisDate, nil},
{"正常日两侧都有", JDECalc(2025, 6, 5), 116.4074, 39.9042, 8, nil, nil},
{"极昼:全天在地平线上", JDCalc(2023, 6, 21), 0, 85, 0, ErrNeverSet, ErrNeverSet},
{"极夜:全天在地平线下", JDCalc(2023, 12, 22), 0, -85, 0, ErrNeverRise, ErrNeverRise},
{"当日无升起但别日有", JDCalc(2024, 2, 29), 0, 60, 0, ErrNotOnThisDate, nil},
{"正常日两侧都有", JDCalc(2025, 6, 5), 116.4074, 39.9042, 8, nil, nil},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -58,8 +58,8 @@ func TestMoonRiseSetMissingEventConvention(t *testing.T) {
func TestMoonRiseSetMissingEventMatchesDailyGeometry(t *testing.T) {
dates := []float64{
JDECalc(2023, 6, 21), JDECalc(2023, 12, 22), JDECalc(2024, 2, 29),
JDECalc(2025, 6, 21), JDECalc(2025, 12, 22), JDECalc(2026, 3, 3),
JDCalc(2023, 6, 21), JDCalc(2023, 12, 22), JDCalc(2024, 2, 29),
JDCalc(2025, 6, 21), JDCalc(2025, 12, 22), JDCalc(2026, 3, 3),
}
latitudes := []float64{-89, -85, -75, -66, -60, 60, 66, 75, 85, 89}
for _, jd := range dates {
+6 -6
View File
@@ -113,7 +113,7 @@ func TestMoonRiseSetMatchesExternalBaselines(t *testing.T) {
if err != nil {
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)
if err != nil {
t.Fatalf("%s current moonrise: %v", sample.Site, err)
@@ -160,7 +160,7 @@ func TestMoonRiseSetLegacyComparatorMatchesPreFixSnapshot(t *testing.T) {
SetDeltaTFn(DefaultDeltaTv2)
defer SetDeltaTFn(previousDeltaT)
jd := JDECalc(2023, 1, 15)
jd := JDCalc(2023, 1, 15)
currentRise, err := GetMoonRiseTime(jd, 116.4074, 39.9042, 8, 1, 0)
if err != nil {
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,
stats *moonRiseSetComparisonStats) {
t.Helper()
current := JDE2DateByZone(currentJD, time.UTC, false)
legacy := JDE2DateByZone(legacyJD, time.UTC, false)
current := JD2DateByZone(currentJD, time.UTC, false)
legacy := JD2DateByZone(legacyJD, time.UTC, false)
horizons := parseMoonRiseSetExternalTime(t, name+".jpl", horizonsUTC)
met := parseMoonRiseSetExternalTime(t, name+".met", metUTC)
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 {
calculationJD := TD2UT(jd-timeZone/24, true)
calculationJD := UTC2TT(jd - timeZone/24)
ra, dec := HMoonTrueRaDecN(calculationJD, -1)
distanceAU := HMoonAwayN(calculationJD, -1) / 149597870.7
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
observerCosine := pcosi(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),
tools.Cos(dec)-observerCosine*horizontalParallaxSine*tools.Cos(hourAngle)) * 180 / math.Pi
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) {
jde := GetNowJDE()
jde := GetNowJD()
for i := 0; i < b.N; i++ {
GetMoonRiseTime(jde, 105, 40, 8, 0, 10)
}
@@ -632,7 +632,7 @@ func TestMoonRiseSetRegression(t *testing.T) {
)
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 {
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",
i, testCase.Year, testCase.Month, testCase.Day,
testCase.Longitude, testCase.Latitude, testCase.TimeZone,
@@ -665,7 +665,7 @@ func TestMoonRiseSetRegression(t *testing.T) {
}
if !setMatches {
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",
i, testCase.Year, testCase.Month, testCase.Day,
testCase.Longitude, testCase.Latitude, testCase.TimeZone,
@@ -715,7 +715,7 @@ func TestMoonRiseSetSpecialCases(t *testing.T) {
for _, tc := range testCases {
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,
tc.timeZone, tc.zenithShift, tc.height)
@@ -751,7 +751,7 @@ func moonRiseSetExpectation(code float64) string {
if err := moonRiseSetExpectedError(code); err != nil {
return err.Error()
}
return JDE2Date(code).String()
return JD2Date(code).String()
}
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 {
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
}
@@ -802,7 +802,7 @@ func floatEquals(a, b, tolerance float64) bool {
// BenchmarkMoonRiseTime 月出时间计算性能测试
func BenchmarkMoonRiseTime(b *testing.B) {
julianDay := JDECalc(2023, 6, 21)
julianDay := JDCalc(2023, 6, 21)
longitude, latitude := 116.4074, 39.9042
timeZone, zenithShift, height := 8.0, 1.0, 0.0
@@ -814,7 +814,7 @@ func BenchmarkMoonRiseTime(b *testing.B) {
// BenchmarkMoonSetTime 月落时间计算性能测试
func BenchmarkMoonSetTime(b *testing.B) {
julianDay := JDECalc(2023, 6, 21)
julianDay := JDCalc(2023, 6, 21)
longitude, latitude := 116.4074, 39.9042
timeZone, zenithShift, height := 8.0, 1.0, 0.0
+4 -4
View File
@@ -8,7 +8,7 @@ import (
)
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
observerLat := 31.2304
@@ -28,8 +28,8 @@ func TestMoonTopocentricPhysicalSampleSweepFiniteAndInRange(t *testing.T) {
observerLat float64
height float64
}{
{"shanghai", TD2UT(Date2JDE(testTime(2026, 4, 28, 9, 30, 45)), true), 121.4737, 31.2304, 4},
{"chicago", TD2UT(Date2JDE(testTime(2024, 3, 25, 7, 0, 0)), true), -87.65, 41.85, 180},
{"shanghai", UTC2TT(Date2JD(testTime(2026, 4, 28, 9, 30, 45))), 121.4737, 31.2304, 4},
{"chicago", UTC2TT(Date2JD(testTime(2024, 3, 25, 7, 0, 0))), -87.65, 41.85, 180},
}
for _, sample := range samples {
@@ -50,7 +50,7 @@ func moonTopocentricPhysicalByCorrection(jd, observerLon, observerLat float64) M
geocentric := MoonPhysical(jd)
moonRA := HMoonTrueRa(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))
Q := ArcTan2(
+63 -63
View File
@@ -7,79 +7,79 @@ import (
. "b612.me/astro/tools"
)
func NeptuneL(jd float64) float64 {
return planet.WherePlanet(7, 0, jd)
func NeptuneL(jde float64) float64 {
return planet.WherePlanet(7, 0, jde)
}
func NeptuneB(jd float64) float64 {
return planet.WherePlanet(7, 1, jd)
func NeptuneB(jde float64) float64 {
return planet.WherePlanet(7, 1, jde)
}
func NeptuneR(jd float64) float64 {
return planet.WherePlanet(7, 2, jd)
func NeptuneR(jde float64) float64 {
return planet.WherePlanet(7, 2, jde)
}
func ANeptuneX(jd float64) float64 {
l := NeptuneL(jd)
b := NeptuneB(jd)
r := NeptuneR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func ANeptuneX(jde float64) float64 {
l := NeptuneL(jde)
b := NeptuneB(jde)
r := NeptuneR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x
}
func ANeptuneY(jd float64) float64 {
func ANeptuneY(jde float64) float64 {
l := NeptuneL(jd)
b := NeptuneB(jd)
r := NeptuneR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
l := NeptuneL(jde)
b := NeptuneB(jde)
r := NeptuneR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y
}
func ANeptuneZ(jd float64) float64 {
//l := NeptuneL(jd)
b := NeptuneB(jd)
r := NeptuneR(jd)
// el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func ANeptuneZ(jde float64) float64 {
//l := NeptuneL(jde)
b := NeptuneB(jde)
r := NeptuneR(jde)
// el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
z := r*Sin(b) - er*Sin(eb)
return z
}
func ANeptuneXYZ(jd float64) (float64, float64, float64) {
l := NeptuneL(jd)
b := NeptuneB(jd)
r := NeptuneR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
func ANeptuneXYZ(jde float64) (float64, float64, float64) {
l := NeptuneL(jde)
b := NeptuneB(jde)
r := NeptuneR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb)
return x, y, z
}
func NeptuneApparentRa(jd float64) float64 {
lo, bo := NeptuneApparentLoBo(jd)
eps := TrueObliquity(jd)
func NeptuneApparentRa(jde float64) float64 {
lo, bo := NeptuneApparentLoBo(jde)
eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
return Limit360(ra)
}
func NeptuneApparentDec(jd float64) float64 {
lo, bo := NeptuneApparentLoBo(jd)
eps := TrueObliquity(jd)
func NeptuneApparentDec(jde float64) float64 {
lo, bo := NeptuneApparentLoBo(jde)
eps := TrueObliquity(jde)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec
}
func NeptuneApparentRaDec(jd float64) (float64, float64) {
lo, bo := NeptuneApparentLoBo(jd)
eps := TrueObliquity(jd)
func NeptuneApparentRaDec(jde float64) (float64, float64) {
lo, bo := NeptuneApparentLoBo(jde)
eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
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
}
func NeptuneMag(jd float64) float64 {
sunDistance := NeptuneR(jd)
earthDistance := EarthNeptuneAway(jd)
earthSunDistance := planet.WherePlanet(-1, 2, jd)
func NeptuneMag(jde float64) float64 {
sunDistance := NeptuneR(jde)
earthDistance := EarthNeptuneAway(jde)
earthSunDistance := planet.WherePlanet(-1, 2, jde)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(i)
mag := -6.87 + 5*math.Log10(sunDistance*earthDistance)
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))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := NeptuneApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
@@ -129,12 +129,12 @@ func NeptuneHeight(jde, lon, lat, timezone float64) float64 {
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))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := NeptuneApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 三角转换公式
@@ -153,21 +153,21 @@ func NeptuneAzimuth(jde, lon, lat, timezone float64) float64 {
}
func NeptuneHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon)
hourAngle := siderealLongitude - NeptuneApparentRa(TD2UT(jd-timezone/24.0, true))
siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - NeptuneApparentRa(UTC2TT(jd-timezone/24.0))
if hourAngle < 0 {
hourAngle += 360
}
return hourAngle
}
func NeptuneCulminationTime(jde, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
func NeptuneCulminationTime(localJD, lon, timezone float64) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5
estimateJD := jde + Limit360(360-NeptuneHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 {
currentHourAngle := NeptuneHourAngle(jde, lon, timezone)
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-NeptuneHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := NeptuneHourAngle(localJD, lon, timezone)
if currentHourAngle < 180 {
currentHourAngle += 360
}
+27 -27
View File
@@ -74,15 +74,15 @@ func neptuneConjunctionFull(jde, degree float64, next uint8) float64 {
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
estimateJDE := jde
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD
longitudeDelta := neptuneSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (neptuneSunLongitudeDelta(prevJD+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 {
prevJDE := estimateJDE
longitudeDelta := neptuneSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (neptuneSunLongitudeDelta(prevJDE+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
@@ -90,7 +90,7 @@ func neptuneConjunctionFull(jde, degree float64, next uint8) float64 {
if !converged {
return math.NaN()
}
return TD2UT(estimateJD, false)
return TT2UTC(estimateJDE)
}
func neptuneConjunction(jde, degree float64, next uint8) float64 {
@@ -105,15 +105,15 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 {
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
estimateJDE := jde
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD
longitudeDelta := neptuneSunLongitudeDeltaN(prevJD, degree, true, neptuneEventSearchN)
longitudeSlope := (neptuneSunLongitudeDeltaN(prevJD+0.000005, degree, true, neptuneEventSearchN) - neptuneSunLongitudeDeltaN(prevJD-0.000005, degree, true, neptuneEventSearchN)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJD) <= neptunePhaseCoarseTolerance {
prevJDE := estimateJDE
longitudeDelta := neptuneSunLongitudeDeltaN(prevJDE, degree, true, neptuneEventSearchN)
longitudeSlope := (neptuneSunLongitudeDeltaN(prevJDE+0.000005, degree, true, neptuneEventSearchN) - neptuneSunLongitudeDeltaN(prevJDE-0.000005, degree, true, neptuneEventSearchN)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= neptunePhaseCoarseTolerance {
converged = true
break
}
@@ -123,12 +123,12 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 {
}
converged = false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD
longitudeDelta := neptuneSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (neptuneSunLongitudeDelta(prevJD+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 {
prevJDE := estimateJDE
longitudeDelta := neptuneSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (neptuneSunLongitudeDelta(prevJDE+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
@@ -136,7 +136,7 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 {
if !converged {
return math.NaN()
}
return TD2UT(estimateJD, false)
return TT2UTC(estimateJDE)
}
func LastNeptuneConjunction(jde float64) float64 {
@@ -175,22 +175,22 @@ func neptuneRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposit
if !isFiniteFloat(oppositionJD) {
return math.NaN()
}
oppositionTT := TD2UT(oppositionJD, true)
oppositionTT := UTC2TT(oppositionJD)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
easternQuadratureUT := neptuneConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
startTT = UTC2TT(easternQuadratureUT)
} else {
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)
}, func(jd float64) float64 {
return neptuneRADerivative(jd, stationDerivativeStepDay)
})
return TD2UT(bestJD, false)
return TT2UTC(bestJDE)
}
func NextNeptuneRetrogradeToPrograde(jde float64) float64 {
+2 -2
View File
@@ -15,8 +15,8 @@ func EclipticObliquity(jde float64, nutation bool) float64 {
return eps
}
func TrueObliquity(JD float64) float64 {
return EclipticObliquity(JD, true)
func TrueObliquity(JDE float64) float64 {
return EclipticObliquity(JDE, true)
}
// 黄经章动 1980
+53 -2
View File
@@ -6,6 +6,8 @@ import (
"math"
"strings"
"time"
"b612.me/astro/tools"
)
// ErrInvalidOccultationInput 表示月掩输入契约无效。
@@ -22,6 +24,8 @@ const (
occultationPathMinimumTargetSpacingKM = 1.0
occultationEventSelectionTolerance = 10 * time.Millisecond
occultationEventSelectionToleranceDays = float64(occultationEventSelectionTolerance) / float64(24*time.Hour)
// 银河系内恒星的径向速度上限,仅用于挡掉明显填错的输入。
starRadialVelocityLimitKmPerSecond = 1000.0
)
// CoordinateFrame 标识恒星输入坐标使用的赤道坐标系。
@@ -155,7 +159,7 @@ func moonTopocentricSemidiameterN(tt float64, observer Observer, n int) float64
if !finite(moonRA) || !finite(moonDec) || !finite(moonDistanceKM) || moonDistanceKM <= 0 {
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 {
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.
// 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 {
return topocentricDistanceKMWithSidereal(ra, dec, distanceKM, observer, ApparentSiderealTime(ut)*15)
return topocentricDistanceKMWithSidereal(ra, dec, distanceKM, observer, ApparentSiderealTime(UTC2UT1(ut))*15)
}
func topocentricDistanceKMWithSidereal(
@@ -231,7 +235,18 @@ type StarCoordinate struct {
ProperMotionRACosDecMasPerYear float64
ProperMotionDecMasPerYear 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 在构造目标前检查恒星坐标契约。
@@ -255,9 +270,26 @@ func (s StarCoordinate) Validate() error {
if !finite(s.ParallaxMas) || s.ParallaxMas < 0 {
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
}
// 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 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,
ProperMotionDecMasPerYear: star.PmDec * 1000,
ParallaxMas: parallaxMas,
RadialVelocityKmPerSecond: star.RadVel,
}
if err := coordinate.Validate(); err != nil {
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.
// Base samples are solved directly; adaptive samples use width interpolation and five-meter error checks.
type OccultationPathPoint struct {
// Time 是该点的时刻。
// Time is the instant the point describes.
Time time.Time
// Longitude 与 Latitude 是地面坐标,东经、北纬为正。
// 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
}
@@ -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 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
NorthernLimit []OccultationPathPoint
SouthernLimit []OccultationPathPoint
@@ -622,7 +671,9 @@ type PlanetOccultationPath struct {
Complete bool
// CenterLine 是影轴与椭球交点的轨迹;非中心事件退化为最接近影轴的椭球点。
// 掠掩事件的路径可以整条没有中心线(只有南北限),此时该切片为空而 Complete 仍可为真。
// 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
// 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.
+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")
}
}
+11 -1
View File
@@ -9,7 +9,11 @@ import (
// 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.
type StarOccultationInstant struct {
// 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
// DeltaTSeconds 本次实际使用的 ΔT / ΔT actually used.
DeltaTSeconds float64
@@ -22,8 +26,14 @@ type StarOccultationInstant struct {
// 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.
type PlanetOccultationInstant struct {
// Time 是本次查询的时刻。
// 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
// DeltaTSeconds 本次实际使用的 ΔT / ΔT actually used.
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
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 {
longitude -= 360
}
+16 -1
View File
@@ -15,6 +15,11 @@ const (
occultationGreatestTimeContourCorrectionIterations = 12
occultationGreatestTimeContourGradientStepDegrees = 1e-4
occultationGreatestTimeContourLatitudeLimitDegrees = 88.0
// 残差量纲随判据口径变化:点源用角距平方(梯度约 1.4e-4/度),行星用外接触度量(约 1/度),
// 同一个绝对阈值在两种口径下相差四个量级——按角距平方给阈值会放过 78 km 的位置偏差。
// 收敛改按位置偏移判定:|残差| / |梯度| 即离零集的地面距离,1e-5 度约 1.1 m,
// 两种口径的残差噪声折算成位置抖动都不超过 2e-7 度。
occultationGreatestTimeContourPositionToleranceDegrees = 1e-5
)
// occultationGreatestTimeArc 固定一个掩甚时刻后的等时线求根器。
@@ -128,6 +133,9 @@ func (arc occultationGreatestTimeArc) correct(longitude, latitude float64) (floa
if denominator < 1e-18 || cosine < 1e-6 {
return 0, 0, state, false
}
if math.Abs(value) <= occultationGreatestTimeContourPositionToleranceDegrees*math.Sqrt(denominator) {
return longitude, latitude, state, true
}
// 完整牛顿步可能一步跨出可见域(掩带很窄,限界附近的种子尤其容易);逐步二分回退,
// 只要还有一步落在域内就继续投影。
scale, advanced := 1.0, false
@@ -150,7 +158,14 @@ func (arc occultationGreatestTimeArc) correct(longitude, latitude float64) (floa
}
}
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 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 条重叠支路)。
func TestOccultationGreatestTimeContoursHaveSingleBranchPerLevel(t *testing.T) {
@@ -16,6 +16,8 @@ const (
occultationLimitExactTolerance = 1e-9
occultationLimitAnchorToleranceKM = 0.5
occultationLimitRatioTolerance = 0.002
// 掠射端点锚点的容差:只钉住端点补采带来的百公里级缺口,不追求末位。
occultationLimitGrazingAnchorToleranceKM = 0.01
)
type occultationLimitFixture struct {
@@ -141,6 +143,11 @@ func occultationLimitIndependentGroundWidth(tt float64, frameAt occultationPathF
consider(vector)
}
}
// 掠射时横向极值恰好落在可见 θ 区间的相切端点上,端点判别式为 0:等差写法的末样本
// 与 interval.right 相差 1 ULP,足以把判别式推成负值而丢掉端点,端点必须按原值补采。
if vector, _, pointOK := occultationPathBoundaryVector(frame, interval.right); pointOK {
consider(vector)
}
}
for index := 0; index < occultationPathBoundaryScanPoints; index++ {
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)
}
}
// 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 {
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1)
moonDistanceKM := HMoonAwayN(tt, -1)
starRA, starDec := starApparentRaDecGeocentric(tt, star)
starDistanceKM := 0.0
if star.ParallaxMas > 0 {
starDistanceKM = 206264806.247 / star.ParallaxMas * occultationPathAstronomicalUnitKM
}
// 方向与距离必须来自同一次三维推进,否则视差修正会退回历元距离。
starRA, starDec, starDistanceAU := starApparentRaDecDistanceGeocentric(tt, star)
starDistanceKM := starDistanceAU * occultationPathAstronomicalUnitKM
return starOccultationEphemerisState{
moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistanceKM,
starRA: starRA, starDec: starDec, starDistanceKM: starDistanceKM,
@@ -994,6 +992,11 @@ func occultationPathScannedLimitsAtFrame(tt float64, frame occultationPathFrame)
consider(vector)
}
}
// 同 occultationPathFiniteCrossTrackExtrema:等差末样本与 rightTheta 差 1 ULP,
// 掠射时端点会被判"无地面交点"而丢掉极值,端点原值必须补采一次。
if vector, _, ok := occultationPathBoundaryVector(frame, rightTheta); ok {
consider(vector)
}
}
}
for i := 0; i < occultationPathBoundaryScanPoints; i++ {
@@ -1258,7 +1261,7 @@ func occultationPathPointFromVectorWithMoon(
location *time.Location,
) OccultationPathPoint {
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) {
ut := TD2UT(tt, false)
return occultationPathGeodeticWithSidereal(vector, ApparentSiderealTime(ut)*15)
ut1 := TT2UT1(tt)
return occultationPathGeodeticWithSidereal(vector, ApparentSiderealTime(ut1)*15)
}
func occultationPathGeodeticWithSidereal(
@@ -1560,7 +1563,7 @@ type occultationPathEarthRotation struct {
}
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)}
}
+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}
})
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)
+2 -2
View File
@@ -370,7 +370,7 @@ func planetMoonPositionAt(tt float64, config planetOccultationConfig, observer *
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, n)
planetRA, planetDec := config.apparentRaDecN(tt, n)
if observer != nil {
ut := TD2UT(tt, false)
ut := TT2UTC(tt)
moonDistanceAU := HMoonAwayN(tt, n) / angularDiameterAstronomicalUnitKM
planetDistanceAU := config.earthDistanceN(tt, n)
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 {
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 {
return math.NaN()
}
+2 -2
View File
@@ -440,7 +440,7 @@ func planetOccultationFootprintAtWithResolution(
if !ok {
return PlanetOccultationFootprint{}, false
}
siderealDegrees := ApparentSiderealTime(TD2UT(tt, false)) * 15
siderealDegrees := ApparentSiderealTime(TT2UT1(tt)) * 15
samples := make([]planetOccultationFootprintSample, boundaryPoints)
for index := range samples {
theta := 2 * math.Pi * float64(index) / float64(len(samples))
@@ -954,7 +954,7 @@ func planetOccultationHorizonCircle(
secondAxis := occultationPathCross(centerDirection, firstAxis)
centerDistance := 1 / distance
circleRadius := math.Sqrt(math.Max(0, 1-centerDistance*centerDistance))
siderealDegrees := ApparentSiderealTime(TD2UT(tt, false)) * 15
siderealDegrees := ApparentSiderealTime(TT2UT1(tt)) * 15
points := make([]OccultationPathPoint, count)
for index := range points {
angle := 2 * math.Pi * float64(index) / float64(count)
+16 -1
View File
@@ -548,7 +548,7 @@ func occultationPathBoundarySamplesAtTimesForFrame(
first := make([]OccultationPathPoint, len(samples))
second := make([]OccultationPathPoint, len(samples))
for index, sample := range samples {
siderealDegrees := ApparentSiderealTime(TD2UT(sample.tt, false)) * 15
siderealDegrees := ApparentSiderealTime(TT2UT1(sample.tt)) * 15
first[index] = occultationPathPointFromVectorWithMoonSidereal(
sample.tt, sample.first, 0, sample.moon, siderealDegrees, location,
)
@@ -1532,6 +1532,11 @@ func occultationPathLimitsAndWidthForFrame(
consider(point)
}
}
// 末样本的等差写法与 rightTheta 相差 1 ULP:掠射时该 ULP 会把端点的判别式推成负值,
// 端点处的横向极值就被当成"无地面交点"丢掉,故端点原值必须补采一次。
if point, _, pointOK := occultationPathBoundaryVector(frame, rightTheta); pointOK {
consider(point)
}
}
}
for i := 0; i < occultationPathBoundaryScanPoints; i++ {
@@ -1641,6 +1646,11 @@ func occultationPathFiniteCrossTrackExtrema(
consider(point)
}
}
// 末样本的等差写法与 rightTheta 相差 1 ULP:掠射时该 ULP 会把端点的判别式推成负值,
// 端点处的极值就被当成"无地面交点"丢掉,故端点原值必须补采一次。
if point, _, pointOK := occultationPathBoundaryVector(frame, rightTheta); pointOK {
consider(point)
}
}
}
for _, interval := range occultationPathBoundaryThetaIntervals(frame) {
@@ -1650,6 +1660,11 @@ func occultationPathFiniteCrossTrackExtrema(
consider(point)
}
}
// 与 occultationPathLimitsAndWidthForFrame 同因:等差末样本与 interval.right 差 1 ULP,
// 掠射时会把端点判别式推成负值而丢掉该处极值,端点原值必须补采一次。
if point, _, pointOK := occultationPathBoundaryVector(frame, interval.right); pointOK {
consider(point)
}
}
if !finite(minimumOffset) || !finite(maximumOffset) {
return occultationPathVector{}, occultationPathVector{}, 0, false
+3 -3
View File
@@ -7,7 +7,7 @@ import (
)
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 {
planet OccultationPlanet
name string
@@ -103,7 +103,7 @@ func TestPlanetOccultationUsesStationMoonDistanceForRadius(t *testing.T) {
t.Fatal("Saturn occultation config is unavailable")
}
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}
far := Observer{Longitude: normalizeLongitude180(subMoonLongitude + 180), Latitude: 0}
nearState := planetOccultationStateAt(tt, config, &near, -1)
@@ -128,7 +128,7 @@ func TestPlanetOccultationUsesStationPlanetDistanceForRadius(t *testing.T) {
t.Fatal("Mercury occultation config is unavailable")
}
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}
far := Observer{Longitude: normalizeLongitude180(subPlanetLongitude + 180), Latitude: 0}
nearState := planetOccultationStateAt(tt, config, &near, -1)
+127 -3
View File
@@ -217,7 +217,7 @@ func newOccultationRiseSetContext(
target := newOccultationRiseSetBody(targetRA, targetDec, targetDistanceKM)
return occultationRiseSetContext{
tt: tt,
siderealDegrees: ApparentSiderealTime(TD2UT(tt, false)) * 15,
siderealDegrees: ApparentSiderealTime(TT2UT1(tt)) * 15,
moonRA: moonRA,
moonDec: moonDec,
moon: moon,
@@ -245,7 +245,7 @@ func newOccultationRiseSetContextFromVectors(
target, _, _, targetOK := occultationRiseSetBodyFromVector(targetXYZ, targetAtFiniteDistance)
return occultationRiseSetContext{
tt: tt,
siderealDegrees: ApparentSiderealTime(TD2UT(tt, false)) * 15,
siderealDegrees: ApparentSiderealTime(TT2UT1(tt)) * 15,
moonRA: moonRA,
moonDec: moonDec,
moon: moon,
@@ -635,7 +635,7 @@ func (evaluation occultationRiseSetEvaluation) pointsAt(
if !evaluation.center.valid || !evaluation.before.valid || !evaluation.after.valid {
return result
}
gst := ApparentSiderealTime(TD2UT(evaluation.tt, false)) * 15
gst := ApparentSiderealTime(TT2UT1(evaluation.tt)) * 15
centerLongitude := normalizeLongitude(evaluation.center.moonRA - gst)
centerLatitude := evaluation.center.moonDec
appendRoots := func(greatest bool) {
@@ -776,6 +776,130 @@ func (evaluation occultationRiseSetEvaluation) classify(
}, 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 {
before := evaluation.before.stateAt(longitude, latitude)
after := evaluation.after.stateAt(longitude, latitude)
+1 -1
View File
@@ -189,7 +189,7 @@ func legacyOccultationRiseSetStateAt(
targetRA, targetDec, targetDistanceKM, targetRadiusKM,
longitude, latitude float64,
) occultationRiseSetState {
siderealDegrees := ApparentSiderealTime(TD2UT(tt, false)) * 15
siderealDegrees := ApparentSiderealTime(TT2UT1(tt)) * 15
observer := Observer{Longitude: longitude, Latitude: latitude}
moonTopocentricRA, moonTopocentricDec := topocentricRaDecWithSidereal(
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 {
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1)
starRA, starDec := starApparentRaDecGeocentric(tt, star)
starRA, starDec, _ := starApparentRaDecDistanceGeocentric(tt, star)
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) {
ra, dec := HMoonGeocentricApparentRaDecN(tt, n)
ut := TD2UT(tt, false)
ut := TT2UTC(tt)
distanceAU := HMoonAwayN(tt, n) / 149597870.7
ra, dec = TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, ut, distanceAU, observer.Height)
return normalizeRA(ra), dec
}
func starApparentRaDec(tt float64, star StarCoordinate, observer Observer) (float64, float64) {
ra, dec := starApparentRaDecGeocentric(tt, star)
if star.ParallaxMas > 0 {
// 1 秒差距处 1 角秒对应 206264.806 AU。
// 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, dec, distanceAU := starApparentRaDecDistanceGeocentric(tt, star)
if distanceAU > 0 {
ra, dec = TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, TT2UTC(tt), distanceAU, observer.Height)
ra = normalizeRA(ra)
}
return ra, dec
}
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)
years := (tt - epochJD) / 365.25
parallaxMas := star.parallaxMas()
ra := star.RA
dec := star.Dec
precessionEpoch := 2451545.0
if star.Frame == CoordinateFrameICRS {
ra, dec = starICRSToMeanJ2000RaDec(ra, dec)
} else if star.Frame == CoordinateFrameApparentOfDate {
ra, dec = starApparentToMeanRaDec(epochJD, ra, dec, star.ParallaxMas)
ra, dec = starApparentToMeanRaDec(epochJD, ra, dec, parallaxMas)
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)
if math.Abs(cosDec) > 1e-12 {
ra += years * star.ProperMotionRACosDecMasPerYear / (3600000.0 * cosDec)
}
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)
return starMeanToApparentRaDec(tt, ra, dec, star.ParallaxMas)
// starProperMotionRaDec3D 按三维匀速直线运动推进:赤经赤纬只是位置矢量的方向。
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) {
@@ -628,7 +690,7 @@ func occultationPositionAngle(moonRA, moonDec, starRA, starDec float64) float64
func occultationAltitude(tt float64, observer Observer, ra, dec float64) float64 {
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 {
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
declination := dec * math.Pi / 180
y := math.Sin(hourAngle)
@@ -666,12 +728,12 @@ func normalizeRA(ra float64) 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 {
if location == nil {
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)
}
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))
meanDec := star.Dec + years*star.ProperMotionDecMasPerYear/3600000
meanRA, meanDec = Precess(meanRA, meanDec, 2451545, tt)
@@ -226,7 +226,7 @@ func TestRefineOccultationPathWidthsBoundsSmoothInterpolationError(t *testing.T)
func TestOccultationPathCachedEarthRotationMatchesDirectGeometry(t *testing.T) {
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}
angle := ApparentSiderealTime(TD2UT(tt, false)) * 15 * math.Pi / 180
angle := ApparentSiderealTime(TT2UT1(tt)) * 15 * math.Pi / 180
want := occultationPathVector{
x: math.Cos(angle)*vector.x + math.Sin(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)
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) {
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
longitude, latitude := normalizeLongitude(coordinates[0]), coordinates[1]
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 {
return [2]float64{}, [2][3]float64{}, false
}
@@ -1002,7 +1007,7 @@ func occultationStationEnvelopeJacobian(
shifted[column] += steps[column]
shiftedTT := referenceTT + shifted[2]/occultationStationEnvelopeTimeScale
shiftedEvaluation := cache.evaluation(shiftedTT)
shiftedResidual, shiftedOK := model.residual(
shiftedResidual, shiftedOK := residualAt(
shiftedEvaluation, normalizeLongitude(shifted[0]), shifted[1],
)
if !shiftedOK {
@@ -1028,6 +1033,30 @@ func (model occultationStationEnvelopeModel) residual(
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 {
if model.kind == occultationStationVisibilityEnvelope {
return occultationStationHorizonResidualToleranceDeg
@@ -1530,7 +1559,7 @@ func occultationStationOracleSampleAt(
) occultationStationBoundarySample {
rotation := occultationPathEarthRotationAt(tt)
inertial := occultationStationInverseEarthRotation(fixed, rotation)
longitude, latitude := occultationPathGeodeticWithSidereal(inertial, ApparentSiderealTime(TD2UT(tt, false))*15)
longitude, latitude := occultationPathGeodeticWithSidereal(inertial, ApparentSiderealTime(TT2UT1(tt))*15)
return occultationStationBoundarySample{
point: OccultationPathPoint{
Time: occultationTTToLocation(tt, location),

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