feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持 - 改进日月食、月掩、行星事件及路径边界计算 - 完善恒星三维自行与动态距离传播 - 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具 - 整理中英文手册、示例资源及回归测试
This commit is contained in:
+81
-2411
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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("零矢量应判为无效")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 除以月球处的地球视差就是它。
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user