2026-05-01 22:38:44 +08:00
package basic
2026-10-15 23:54:15 +08:00
import (
"math"
"b612.me/astro/internal/ephem"
"b612.me/astro/internal/geodata"
)
2026-05-01 22:38:44 +08:00
// SolarEclipseRadiusModel 表示日食计算中月亮平均半径 k 的取法。
type SolarEclipseRadiusModel string
const (
// SolarEclipseModelIAUSingleK 使用 IAU 单一月亮平均半径 k。
SolarEclipseModelIAUSingleK SolarEclipseRadiusModel = "iau_single_k"
// SolarEclipseModelNASABulletinSplitK 使用 NASA bulletin 的 Split-K 口径。
SolarEclipseModelNASABulletinSplitK SolarEclipseRadiusModel = "nasa_bulletin_split_k"
)
2026-09-23 18:55:12 +08:00
// SolarEclipseSunRadiusModel 日食几何的太阳半径口径 / solar radius convention for eclipse geometry.
type SolarEclipseSunRadiusModel string
const (
// SolarEclipseSunRadiusStandard 标准档,1 AU 处 959.639″,复现已发布星历表与目录 / standard.
SolarEclipseSunRadiusStandard SolarEclipseSunRadiusModel = "standard"
// SolarEclipseSunRadiusMeasured 边缘档,1 AU 处 959.95″:全食带每侧约窄 0.6 千米、中心食时长约短 1.5 秒 / measured.
SolarEclipseSunRadiusMeasured SolarEclipseSunRadiusModel = "measured"
)
// SolarEclipseOptions 日食计算的半径口径 / radius conventions for a solar eclipse computation.
type SolarEclipseOptions struct {
// RadiusModel 月亮平均半径 k 的口径,零值为 NASA bulletin Split-K / lunar radius model.
RadiusModel SolarEclipseRadiusModel
// SunRadiusModel 太阳半径口径,零值为标准档 / solar radius convention.
SunRadiusModel SolarEclipseSunRadiusModel
2026-10-15 23:54:15 +08:00
// LunarLimb 月缘剖面,nil 为圆月面;修正站心接触、食甚时的站心食分与遮蔽率,以及全球偏食始末与食甚点中心食时长 / lunar limb profile for local contacts, magnitude and obscuration, global P1/P4 and central duration.
LunarLimb LunarLimb
2026-09-23 18:55:12 +08:00
}
2026-09-17 12:27:40 +08:00
// SolarEclipseType 整场日食的全局食型。
2026-05-01 22:38:44 +08:00
type SolarEclipseType string
const (
// SolarEclipseNone 表示该次朔月没有发生日食。
SolarEclipseNone SolarEclipseType = "none"
// SolarEclipsePartial 表示日偏食。
SolarEclipsePartial SolarEclipseType = "partial"
// SolarEclipseAnnular 表示日环食。
SolarEclipseAnnular SolarEclipseType = "annular"
// SolarEclipseTotal 表示日全食。
SolarEclipseTotal SolarEclipseType = "total"
// SolarEclipseHybrid 表示全环食/混合食。
SolarEclipseHybrid SolarEclipseType = "hybrid"
)
// SolarEclipseCentrality 表示中心线进入地球的方式。
type SolarEclipseCentrality string
const (
// SolarEclipseNonCentral 表示无中心线进入地球。
SolarEclipseNonCentral SolarEclipseCentrality = "non_central"
// SolarEclipseCentralOneLimit 表示中心线只形成一侧极限条件。
SolarEclipseCentralOneLimit SolarEclipseCentrality = "central_one_limit"
// SolarEclipseCentralTwoLimits 表示中心线完整进入地球,两侧都有界线。
SolarEclipseCentralTwoLimits SolarEclipseCentrality = "central_two_limits"
)
// SolarEclipseResult 表示一次朔月附近的全局日食几何结果。
//
// 所有时刻字段都使用力学时儒略日(JDE, TT)。
// 输入 seedJDE 只需要落在目标朔月附近,允许相差数天。
type SolarEclipseResult struct {
2026-09-23 18:55:12 +08:00
// 下列字段是决定上述数值的口径,随结果一起保留。
// The fields below are the conventions that fix the numbers above.
Model SolarEclipseRadiusModel
SunRadiusModel SolarEclipseSunRadiusModel
2026-10-15 23:54:15 +08:00
// LunarLimbApplied 表示接触或食甚点中心食时长已按月缘剖面修正 / reports whether lunar-limb refinement changed the result.
LunarLimbApplied bool
// PartialBeginContact / PartialEndContact 是月缘修正后的偏食始末接触点位置,未修正时 Defined 为 false。
// The limb-corrected locations of the global partial contacts; Defined is false without a correction.
PartialBeginContact SolarEclipseLimbContact
PartialEndContact SolarEclipseLimbContact
Type SolarEclipseType
Centrality SolarEclipseCentrality
2026-05-01 22:38:44 +08:00
// GreatestEclipse 是全局“影轴最接近地心”的时刻。
GreatestEclipse float64
// PartialBeginOnEarth / PartialEndOnEarth 是地球范围的偏食开始 / 结束时刻。
PartialBeginOnEarth float64
PartialEndOnEarth float64
// CentralBeginOnEarth / CentralEndOnEarth 是中心线进入 / 离开地球的时刻。
CentralBeginOnEarth float64
CentralEndOnEarth float64
// Magnitude 是全局食分。
Magnitude float64
// Gamma 是月影轴到地心的有符号最小距离,单位为地球赤道半径。
Gamma float64
2026-09-17 12:27:40 +08:00
// CentralDurationDays 是食甚点的中心食持续时间,单位为日;没有中心食时为 0。
// 这是日食目录(如 NASA「Central Dur.」)采用的口径:食甚点的中心食时长。
// CentralDurationDays is the central-phase duration at the greatest eclipse,
// in days, and 0 when the event has no central phase.
CentralDurationDays float64
2026-09-23 18:55:12 +08:00
// PathWidthKM 是食甚处中心食带宽度;非中心食为 0;单侧极限(中心带仅触及地球边缘)时该解析式
// 失效并一并置 0,此时 PathWidthDefined 为 false, NASA 目录该栏印 '-'。
// PathWidthKM is the central path width at greatest eclipse, 0 for a non-central
// event, and 0 when the analytic formula fails at a single-sided limit where the
// band only grazes the Earth's limb; PathWidthDefined is false there and
// catalogues print '-' for this column.
2026-05-01 22:38:44 +08:00
PathWidthKM float64
2026-09-23 18:55:12 +08:00
// PathWidthDefined 表示上面的带宽是否有定义:只有南北两限都存在(central_two_limits)时才为 true。
// PathWidthDefined reports whether the width above is defined: it is true only
// when both band limits exist, that is for central_two_limits.
PathWidthDefined bool
2026-05-01 22:38:44 +08:00
// GreatestLongitude / GreatestLatitude 是日食食甚点地理坐标,东经为正,西经为负。
GreatestLongitude float64
GreatestLatitude float64
HasPartial bool
HasCentral bool
HasAnnular bool
HasTotal bool
HasHybrid bool
}
type solarEclipseModelParameters struct {
2026-09-23 18:55:12 +08:00
penumbralK float64
umbralK float64
sunRadiusRatio float64
2026-10-15 23:54:15 +08:00
// lunarLimb 非 nil 时站心接触改用月缘剖面的方向半径。
lunarLimb LunarLimb
2026-05-01 22:38:44 +08:00
}
type solarEclipseShadowRadii struct {
penumbraRadius float64
umbraRadius float64
absUmbraRadius float64
magnitude float64
}
type solarEclipseAxis struct {
rightAscension float64
tilt float64
gst float64
}
type solarEclipseSolver struct {
2026-10-15 23:54:15 +08:00
inst * Instance
2026-09-23 18:55:12 +08:00
newMoonJDE float64
model SolarEclipseRadiusModel
sunRadiusModel SolarEclipseSunRadiusModel
params solarEclipseModelParameters
2026-05-01 22:38:44 +08:00
2026-09-17 12:27:40 +08:00
localStateContextCache map [ uint64 ] localSolarEclipseStateContext
localEphemeris * solarEclipseLocalEphemeris
2026-10-15 23:54:15 +08:00
// limbAxes 与 localEphemeris 同网格的月球本体坐标架,仅在设置月缘时构造。
limbAxes * solarEclipseLimbAxes
2026-09-17 12:27:40 +08:00
// deltaTSeconds 是调用方显式给出的 ΔT(秒);NaN 表示未覆盖,用进程级模型。
// 只影响地球自转相位(轴的 gst),不改变任何 TT 时刻。
deltaTSeconds float64
besselGeometryCache map [ uint64 ] solarEclipseBesselGeometryCacheEntry
besselCandidateCache map [ uint64 ] solarEclipseBesselGeometryCacheEntry
exactCentralContact bool
2026-05-01 22:38:44 +08:00
meanSunMoonDistance float64
penumbraConeTangent float64
umbraConeTangent float64
}
2026-09-17 12:27:40 +08:00
const solarEclipseBesselGeometryCacheMaximumEntries = movingDiskEventCacheMaximumEntries
// solarEclipseBesselGeometryCacheEntry keeps exact and candidate geometry in
// separate maps. Candidate geometry is interpolated and is only suitable for
// coarse scans; mixing it with exact geometry would silently reduce contact
// and topology accuracy.
type solarEclipseBesselGeometryCacheEntry struct {
// generation 记录写入时的 ΔT 世代:轴里的 gst 由 ΔT 决定,ΔT 覆盖后条目必须失效。
// generation is the ΔT generation at write time: the axis carries a ΔT-dependent
// gst, so overriding ΔT has to invalidate the entry.
generation uint64
moon [ 3 ] float64
axis solarEclipseAxis
sun [ 3 ] float64
valid bool
}
2026-05-01 22:38:44 +08:00
type solarEclipseFeature struct {
greatestEclipseJDE float64
greatestLongitude float64
greatestLatitude float64
magnitude float64
gamma float64
pathWidthKM float64
partialBeginJDE float64
partialEndJDE float64
centralBeginJDE float64
centralEndJDE float64
typeCode string
}
type solarEclipseLineIntersection struct {
valid bool
x float64
y float64
z float64
r1 float64
r2 float64
}
const (
2026-09-17 12:27:40 +08:00
// 赤道自转线速度,用于把 ΔT 误差换算成地面横移(见 DeltaTGroundShiftKM)。
// Equatorial rotation speed, used to convert a ΔT error into ground displacement.
solarEclipseEarthEquatorialRotationKMPerSecond = 0.4651
solarEclipseEarthPolarRatio = 0.99664719
solarEclipseEarthPolarRatioSquared = solarEclipseEarthPolarRatio * solarEclipseEarthPolarRatio
solarEclipseAstronomicalUnitKM = 1.49597870691e8
2026-05-01 22:38:44 +08:00
2026-09-23 18:55:12 +08:00
// 标准档与边缘档在 1 AU 处的太阳视半径(角秒),日食与月食几何共用这一组常量。
eclipseSunRadiusStandardArcsec = 959.639
eclipseSunRadiusMeasuredArcsec = 959.95
// 标准档太阳半径是地球赤道半径的 109.1222 倍,与上面的标准档视半径等价;边缘档按视半径比例放大。
solarEclipseSunRadiusRatioStandard = 109.1222
solarEclipseSunRadiusRatioMeasured = solarEclipseSunRadiusRatioStandard * eclipseSunRadiusMeasuredArcsec / eclipseSunRadiusStandardArcsec
// Split-K:半影(偏食)0.2724880、本影与反本影 0.2722810; IAU Single-K 全部使用 0.2725076。
solarEclipsePenumbralK = 0.2724880
2026-05-01 22:38:44 +08:00
solarEclipseUmbralK = 0.2722810
2026-09-23 18:55:12 +08:00
solarEclipseIAUSingleRadiusK = 0.2725076
// SolarEclipsePenumbralK / SolarEclipseUmbralK 是 Split-K 的半影与本影月地半径比 k1/k2,
// SolarEclipseIAUSingleRadiusK 是 IAU Single-K 的单一值。
// SolarEclipsePenumbralK / SolarEclipseUmbralK are the split-k lunar-to-terrestrial radius ratios,
// SolarEclipseIAUSingleRadiusK the IAU single value.
SolarEclipsePenumbralK = solarEclipsePenumbralK
SolarEclipseUmbralK = solarEclipseUmbralK
SolarEclipseIAUSingleRadiusK = solarEclipseIAUSingleRadiusK
2026-09-17 12:27:40 +08:00
solarEclipseNodeCount = 7
solarEclipseNodeStepDays = 0.04
solarEclipseMoonLonAberrRad = - 3.4e-6
solarEclipseAxisContactInitialStepDays = 1.0 / 86400.0
solarEclipseAxisContactMaximumStepDays = 30.0 / 1440.0
solarEclipseAxisContactToleranceDays = 1e-9
2026-05-01 22:38:44 +08:00
// 这两个系数沿用经典贝塞尔近似中的极区有效半径经验值。
solarEclipseNonCentralLimit = 0.9972
solarEclipseCentralLimit = 0.9966
)
var solarEclipseArcsecPerRadian = 180.0 * 3600.0 / math . Pi
2026-09-23 18:55:12 +08:00
func normalizeSolarEclipseRadiusModel ( model SolarEclipseRadiusModel ) SolarEclipseRadiusModel {
if model == SolarEclipseModelIAUSingleK {
return SolarEclipseModelIAUSingleK
}
return SolarEclipseModelNASABulletinSplitK
}
func normalizeSolarEclipseSunRadiusModel ( model SolarEclipseSunRadiusModel ) SolarEclipseSunRadiusModel {
if model == SolarEclipseSunRadiusMeasured {
return SolarEclipseSunRadiusMeasured
}
return SolarEclipseSunRadiusStandard
}
func solarEclipseSunRadiusRatio ( model SolarEclipseSunRadiusModel ) float64 {
if normalizeSolarEclipseSunRadiusModel ( model ) == SolarEclipseSunRadiusMeasured {
return solarEclipseSunRadiusRatioMeasured
}
return solarEclipseSunRadiusRatioStandard
}
2026-10-15 23:54:15 +08:00
// SolarEclipseSunSemidiameterWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func SolarEclipseSunSemidiameterWithInstance ( jde float64 , model SolarEclipseSunRadiusModel , inst * Instance ) float64 {
return angularSemidiameterFromAU ( solarEclipseSunRadiusRatio ( model ) * geodata . EarthEquatorialRadiusKM , EarthAwayNWithInstance ( jde , - 1 , inst ))
2026-09-23 18:55:12 +08:00
}
// SolarEclipse 计算给定近朔时刻附近的一次全局日食,默认使用 NASABulletin Split-K 模型与标准太阳半径。
2026-05-01 22:38:44 +08:00
func SolarEclipse ( seedJDE float64 ) SolarEclipseResult {
return SolarEclipseNASABulletinSplitK ( seedJDE )
}
2026-10-15 23:54:15 +08:00
// SolarEclipseWithOptionsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func SolarEclipseWithOptionsWithInstance ( seedJDE float64 , options SolarEclipseOptions , inst * Instance ) SolarEclipseResult {
if options . LunarLimb == nil {
return solarEclipseWithDeltaT ( seedJDE , options , 0 , inst )
}
limb := options . LunarLimb
options . LunarLimb = nil
newMoonJDE := solarEclipseAnchorNewMoon ( seedJDE , inst )
solver := newSolarEclipseSolverWithOptions ( newMoonJDE , options , inst )
return solver . withLunarLimb ( solver . eclipseResult (), limb )
2026-09-23 18:55:12 +08:00
}
2026-10-15 23:54:15 +08:00
// SolarEclipseIAUSingleKWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func SolarEclipseIAUSingleKWithInstance ( seedJDE float64 , inst * Instance ) SolarEclipseResult {
return solarEclipse ( seedJDE , SolarEclipseModelIAUSingleK , inst )
2026-05-01 22:38:44 +08:00
}
2026-10-15 23:54:15 +08:00
// SolarEclipseNASABulletinSplitKWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func SolarEclipseNASABulletinSplitKWithInstance ( seedJDE float64 , inst * Instance ) SolarEclipseResult {
return solarEclipse ( seedJDE , SolarEclipseModelNASABulletinSplitK , inst )
2026-05-01 22:38:44 +08:00
}
2026-10-15 23:54:15 +08:00
func solarEclipse ( seedJDE float64 , model SolarEclipseRadiusModel , instances ...* Instance ) SolarEclipseResult {
inst := instanceValue ( instances )
return solarEclipseWithDeltaT ( seedJDE , SolarEclipseOptions { RadiusModel : model }, 0 , inst )
2026-09-17 12:27:40 +08:00
}
func solarEclipseWithDeltaT (
seedJDE float64 ,
2026-09-23 18:55:12 +08:00
options SolarEclipseOptions ,
2026-10-15 23:54:15 +08:00
deltaTSeconds float64 , instances ...* Instance ) SolarEclipseResult {
inst := instanceValue ( instances )
newMoonJDE := solarEclipseAnchorNewMoon ( seedJDE , inst )
solver := newSolarEclipseSolverWithOptions ( newMoonJDE , options , inst ). withDeltaTSeconds ( deltaTSeconds )
2026-09-17 12:27:40 +08:00
return solver . eclipseResult ()
}
func ( solver solarEclipseSolver ) eclipseResult () SolarEclipseResult {
model := solver . model
2026-05-01 22:38:44 +08:00
feature := solver . feature ()
result := SolarEclipseResult {
Model : model ,
2026-09-23 18:55:12 +08:00
SunRadiusModel : solver . sunRadiusModel ,
2026-05-01 22:38:44 +08:00
Type : SolarEclipseNone ,
Centrality : SolarEclipseNonCentral ,
GreatestEclipse : feature . greatestEclipseJDE ,
Magnitude : feature . magnitude ,
Gamma : feature . gamma ,
PathWidthKM : feature . pathWidthKM ,
GreatestLongitude : feature . greatestLongitude ,
GreatestLatitude : feature . greatestLatitude ,
}
switch feature . typeCode {
case "P" :
result . Type = SolarEclipsePartial
case "A0" , "A1" , "A" :
result . Type = SolarEclipseAnnular
case "T0" , "T1" , "T" :
result . Type = SolarEclipseTotal
case "H" , "H2" , "H3" :
result . Type = SolarEclipseHybrid
}
2026-09-23 18:55:12 +08:00
if solarEclipseTwoLimitsTypeCode ( feature . typeCode ) {
2026-05-01 22:38:44 +08:00
result . Centrality = SolarEclipseCentralTwoLimits
2026-09-23 18:55:12 +08:00
} else if feature . typeCode == "A1" || feature . typeCode == "T1" {
result . Centrality = SolarEclipseCentralOneLimit
2026-05-01 22:38:44 +08:00
}
2026-09-23 18:55:12 +08:00
result . PathWidthDefined = result . Centrality == SolarEclipseCentralTwoLimits
2026-05-01 22:38:44 +08:00
if result . Type != SolarEclipseNone {
result . HasPartial = true
result . PartialBeginOnEarth = feature . partialBeginJDE
result . PartialEndOnEarth = feature . partialEndJDE
}
if result . Centrality != SolarEclipseNonCentral {
result . HasCentral = true
result . CentralBeginOnEarth = feature . centralBeginJDE
result . CentralEndOnEarth = feature . centralEndJDE
2026-09-17 12:27:40 +08:00
result . CentralDurationDays = solver . greatestCentralDuration ( result )
2026-05-01 22:38:44 +08:00
}
switch result . Type {
case SolarEclipseAnnular :
result . HasAnnular = true
case SolarEclipseTotal :
result . HasTotal = true
case SolarEclipseHybrid :
result . HasAnnular = true
result . HasTotal = true
result . HasHybrid = true
}
return result
}
2026-09-17 12:27:40 +08:00
// greatestCentralDuration 在食甚点解一次站心中心食并返回中心相时长(日);没有中心相时为 0。
// greatestCentralDuration solves the local eclipse at the greatest eclipse point and
// returns its central-phase duration in days.
func ( solver solarEclipseSolver ) greatestCentralDuration ( result SolarEclipseResult ) float64 {
if ! result . HasCentral || result . GreatestEclipse <= 0 {
return 0
}
return solver . centralPhaseDurationDaysAt (
result . GreatestEclipse , result . GreatestLongitude , result . GreatestLatitude ,
)
}
2026-05-01 22:38:44 +08:00
func newSolarEclipseSolver ( newMoonJDE float64 , model SolarEclipseRadiusModel ) solarEclipseSolver {
2026-09-23 18:55:12 +08:00
return newSolarEclipseSolverWithOptions ( newMoonJDE , SolarEclipseOptions { RadiusModel : model })
}
2026-10-15 23:54:15 +08:00
func newSolarEclipseSolverWithOptions ( newMoonJDE float64 , options SolarEclipseOptions , instances ...* Instance ) solarEclipseSolver {
inst := instanceValue ( instances )
2026-09-23 18:55:12 +08:00
options . RadiusModel = normalizeSolarEclipseRadiusModel ( options . RadiusModel )
options . SunRadiusModel = normalizeSolarEclipseSunRadiusModel ( options . SunRadiusModel )
params := solarEclipseModelParams ( options . RadiusModel , options . SunRadiusModel )
2026-05-01 22:38:44 +08:00
firstNodeJDE := newMoonJDE + ( 0 - float64 ( solarEclipseNodeCount ) / 2 + 0.5 ) * solarEclipseNodeStepDays
lastNodeJDE := newMoonJDE + ( float64 ( solarEclipseNodeCount - 1 ) - float64 ( solarEclipseNodeCount ) / 2 + 0.5 ) * solarEclipseNodeStepDays
2026-10-15 23:54:15 +08:00
firstSun , firstMoon := solarEclipseSunMoonEquatorial ( firstNodeJDE , inst )
lastSun , lastMoon := solarEclipseSunMoonEquatorial ( lastNodeJDE , inst )
2026-05-01 22:38:44 +08:00
2026-10-15 23:54:15 +08:00
meanSunMoonDistance := (( firstSun [ 2 ] + lastSun [ 2 ]) - ( firstMoon [ 2 ] + lastMoon [ 2 ])) / 2 / geodata . EarthEquatorialRadiusKM
2026-05-01 22:38:44 +08:00
2026-10-15 23:54:15 +08:00
return solarEclipseSolver { inst : inst ,
2026-09-17 12:27:40 +08:00
newMoonJDE : newMoonJDE ,
2026-09-23 18:55:12 +08:00
model : options . RadiusModel ,
sunRadiusModel : options . SunRadiusModel ,
2026-09-17 12:27:40 +08:00
params : params ,
deltaTSeconds : math . NaN (),
localStateContextCache : make ( map [ uint64 ] localSolarEclipseStateContext ),
besselGeometryCache : make ( map [ uint64 ] solarEclipseBesselGeometryCacheEntry ),
besselCandidateCache : make ( map [ uint64 ] solarEclipseBesselGeometryCacheEntry ),
meanSunMoonDistance : meanSunMoonDistance ,
2026-09-23 18:55:12 +08:00
penumbraConeTangent : ( params . sunRadiusRatio + params . penumbralK ) / meanSunMoonDistance ,
umbraConeTangent : ( params . sunRadiusRatio - params . umbralK ) / meanSunMoonDistance ,
2026-09-17 12:27:40 +08:00
}
}
// withDeltaTSeconds 固定本求解器使用的 ΔT(秒),非正值表示回到进程级模型。覆盖会改变
// 轴里的 gst,因此所有按精确 float 位键控的几何缓存必须同时作废。
func ( solver solarEclipseSolver ) withDeltaTSeconds ( deltaTSeconds float64 ) solarEclipseSolver {
if deltaTSeconds <= 0 || math . IsNaN ( deltaTSeconds ) || math . IsInf ( deltaTSeconds , 0 ) {
deltaTSeconds = math . NaN ()
}
solver . deltaTSeconds = deltaTSeconds
solver . besselGeometryCache = make ( map [ uint64 ] solarEclipseBesselGeometryCacheEntry )
solver . besselCandidateCache = make ( map [ uint64 ] solarEclipseBesselGeometryCacheEntry )
solver . localStateContextCache = make ( map [ uint64 ] localSolarEclipseStateContext )
return solver
}
// effectiveDeltaTSeconds 返回本求解器在某 TT 时刻实际使用的 ΔT(秒)。
2026-09-23 18:55:12 +08:00
// 未覆盖时用真 TT−UT1(观测表/外推),不能回退到混入 UTC 的进程级 DeltaT,
// 否则恒星时相位会少掉 DUT1,站心与影轴两条路径就不一致。
func ( solver solarEclipseSolver ) effectiveDeltaTSeconds ( jde float64 ) float64 {
2026-09-17 12:27:40 +08:00
if math . IsNaN ( solver . deltaTSeconds ) {
2026-09-23 18:55:12 +08:00
return ut1ToTTOffsetSeconds ( ttToUT1JDE ( jde ))
2026-05-01 22:38:44 +08:00
}
2026-09-17 12:27:40 +08:00
return solver . deltaTSeconds
}
// siderealTimeAt 返回某 TT 时刻的视恒星时(弧度),ΔT 覆盖时同样生效。
2026-09-23 18:55:12 +08:00
func ( solver solarEclipseSolver ) siderealTimeAt ( jde float64 ) float64 {
ut1JDE := TT2UT1 ( jde )
2026-09-17 12:27:40 +08:00
if ! math . IsNaN ( solver . deltaTSeconds ) {
2026-09-23 18:55:12 +08:00
ut1JDE = jde - solver . deltaTSeconds / 86400
2026-09-17 12:27:40 +08:00
}
2026-09-23 18:55:12 +08:00
return ApparentSiderealTime ( ut1JDE ) * 15 * rad
2026-09-17 12:27:40 +08:00
}
// withLocalEphemeris prepares the immutable event-local interpolator used by
// coarse candidate scans. The exact ephemeris remains the fallback outside its
// bounded window and is used by all contact and topology refinements.
func ( solver solarEclipseSolver ) withLocalEphemeris () solarEclipseSolver {
if solver . localEphemeris == nil {
2026-10-15 23:54:15 +08:00
solver . localEphemeris = newSolarEclipseLocalEphemeris ( solver . newMoonJDE , solver . inst )
}
if solver . limbAxes == nil && solver . params . lunarLimb != nil {
solver . limbAxes = newSolarEclipseLimbAxes ( solver . localEphemeris , solver . params . lunarLimb , solver . inst )
2026-09-17 12:27:40 +08:00
}
return solver
2026-05-01 22:38:44 +08:00
}
2026-09-23 18:55:12 +08:00
// solarEclipseTwoLimitsTypeCode 报告该类型码的南北两限是否都存在,带宽解析式只在这一类中心食上有定义。
func solarEclipseTwoLimitsTypeCode ( typeCode string ) bool {
switch typeCode {
case "A" , "T" , "H" , "H2" , "H3" :
return true
}
return false
}
2026-05-01 22:38:44 +08:00
func ( solver solarEclipseSolver ) feature () solarEclipseFeature {
const finiteDifferenceStep = 0.04
2026-09-17 12:27:40 +08:00
candidateSolver := solver . withLocalEphemeris ()
2026-05-01 22:38:44 +08:00
2026-09-23 18:55:12 +08:00
jde := solver . newMoonJDE
before := candidateSolver . besselMoonCandidateAt ( jde - finiteDifferenceStep )
center := candidateSolver . besselMoonCandidateAt ( jde )
after := candidateSolver . besselMoonCandidateAt ( jde + finiteDifferenceStep )
2026-05-01 22:38:44 +08:00
vx := ( after [ 0 ] - before [ 0 ]) / ( 2 * finiteDifferenceStep )
vy := ( after [ 1 ] - before [ 1 ]) / ( 2 * finiteDifferenceStep )
vz := ( after [ 2 ] - before [ 2 ]) / ( 2 * finiteDifferenceStep )
speed := math . Hypot ( vx , vy )
speedSquared := speed * speed
t0 := - ( center [ 0 ] * vx + center [ 1 ] * vy ) / speedSquared
2026-09-23 18:55:12 +08:00
greatestEclipseJDE := jde + t0
2026-09-17 12:27:40 +08:00
// The three-node velocity fit locates greatest eclipse accurately, but its
// linearly extrapolated coordinates can miss the true Bessel position by
// tens of kilometres in a grazing non-central event. Re-evaluate the
// ephemeris at the solved time before deriving surface coordinates and
// shadow radii so markers and path geometry use the same state.
greatestMoon := solver . besselMoonAt ( greatestEclipseJDE )
xc := greatestMoon [ 0 ]
yc := greatestMoon [ 1 ]
zc := greatestMoon [ 2 ]
2026-05-01 22:38:44 +08:00
gamma := ( vx * center [ 1 ] - vy * center [ 0 ]) / speed
minimumDistance := math . Abs ( gamma )
axis := solver . besselAxisAt ( greatestEclipseJDE )
axisIntersection := solarEclipseLineEar2 ( xc , yc , 2 , xc , yc , 0 , solarEclipseEarthPolarRatio , 1 , axis )
midRadii := solver . shadowRadiiAt ( zc )
greatestRadii := midRadii
if axisIntersection . valid {
greatestRadii = solver . shadowRadiiAt ( zc - axisIntersection . r2 )
}
var centralStartParam , centralEndParam float64
if minimumDistance < 1 {
paramSpan := math . Sqrt ( 1 - minimumDistance * minimumDistance ) / speed
centralStartParam = t0 - paramSpan
centralEndParam = t0 + paramSpan
}
partialLimit := 1 + midRadii . penumbraRadius
partialSpan := 0.0
if minimumDistance < partialLimit {
partialSpan = math . Sqrt ( partialLimit * partialLimit - minimumDistance * minimumDistance ) / speed
}
partialStartParam := t0 - partialSpan
partialEndParam := t0 + partialSpan
typeCode := "N"
greatestLongitude , greatestLatitude := 0.0 , 0.0
magnitude := 0.0
pathWidthKM := 0.0
if ! axisIntersection . valid {
greatestLongitude , greatestLatitude = solarEclipseBesselPointToGeodetic ( xc , yc , 0 , axis , false )
magnitude = ( midRadii . penumbraRadius - ( minimumDistance - solarEclipseNonCentralLimit )) / ( midRadii . penumbraRadius - midRadii . umbraRadius )
switch {
case minimumDistance > solarEclipseNonCentralLimit + midRadii . penumbraRadius :
typeCode = "N"
case minimumDistance > solarEclipseNonCentralLimit + midRadii . absUmbraRadius :
typeCode = "P"
default :
if midRadii . magnitude < 1 {
typeCode = "A0"
} else {
typeCode = "T0"
}
}
} else {
greatestLongitude = axisIntersectionLongitude ( axisIntersection , axis )
greatestLatitude = axisIntersectionLatitude ( axisIntersection , axis )
magnitude = greatestRadii . magnitude
switch {
case minimumDistance > solarEclipseCentralLimit - greatestRadii . absUmbraRadius :
if greatestRadii . magnitude < 1 {
typeCode = "A1"
} else {
typeCode = "T1"
}
default :
if greatestRadii . magnitude >= 1 {
startRadii := greatestRadii
endRadii := greatestRadii
if minimumDistance < 1 {
startRadii = solver . shadowRadiiAt ( centralStartParam * vz + center [ 2 ] - 1.37 * centralStartParam * centralStartParam )
endRadii = solver . shadowRadiiAt ( centralEndParam * vz + center [ 2 ] - 1.37 * centralEndParam * centralEndParam )
}
typeCode = "H"
if startRadii . magnitude > 1 {
typeCode = "H2"
}
if endRadii . magnitude > 1 {
typeCode = "H3"
}
if startRadii . magnitude > 1 && endRadii . magnitude > 1 {
typeCode = "T"
}
} else {
typeCode = "A"
}
}
2026-09-23 18:55:12 +08:00
// 单侧极限(A1/T1)只有一侧限界,非中心中心食(A0/T0)连限界都没有:解析式 2r/|sin h|
// 在 h→0 时发散,两类事件该栏都无定义,与中心线逐点宽度一起置 0。
if solarEclipseTwoLimitsTypeCode ( typeCode ) {
2026-10-15 23:54:15 +08:00
sunAltitude := solarEclipseSunAltitudeAtGreatest ( greatestEclipseJDE , greatestLongitude , greatestLatitude , axis . gst , solver . inst )
2026-05-01 22:38:44 +08:00
if math . Abs ( math . Sin ( sunAltitude )) > 1e-12 {
2026-10-15 23:54:15 +08:00
pathWidthKM = math . Abs ( 2 * greatestRadii . umbraRadius * geodata . EarthEquatorialRadiusKM ) / math . Abs ( math . Sin ( sunAltitude ))
2026-05-01 22:38:44 +08:00
}
}
}
feature := solarEclipseFeature {
greatestEclipseJDE : greatestEclipseJDE ,
greatestLongitude : greatestLongitude ,
greatestLatitude : greatestLatitude ,
magnitude : magnitude ,
gamma : gamma ,
pathWidthKM : pathWidthKM ,
typeCode : typeCode ,
}
if typeCode != "N" {
2026-09-23 18:55:12 +08:00
_ , _ , feature . partialBeginJDE , _ = solver . quickContactAt ( partialStartParam + jde , vx , vy , true )
_ , _ , feature . partialEndJDE , _ = solver . quickContactAt ( partialEndParam + jde , vx , vy , true )
2026-05-01 22:38:44 +08:00
}
2026-09-17 12:27:40 +08:00
if axisIntersection . valid && typeCode != "N" && typeCode != "P" {
2026-09-23 18:55:12 +08:00
_ , _ , feature . centralBeginJDE , _ = solver . quickContactAt ( centralStartParam + jde , vx , vy , false )
_ , _ , feature . centralEndJDE , _ = solver . quickContactAt ( centralEndParam + jde , vx , vy , false )
2026-09-17 12:27:40 +08:00
if refined , ok := solver . centralAxisContactJDE ( feature . centralBeginJDE , greatestEclipseJDE , - 1 ); ok {
feature . centralBeginJDE = refined
}
if refined , ok := solver . centralAxisContactJDE ( feature . centralEndJDE , greatestEclipseJDE , 1 ); ok {
feature . centralEndJDE = refined
}
2026-05-01 22:38:44 +08:00
}
return feature
}
2026-09-17 12:27:40 +08:00
func ( solver solarEclipseSolver ) centralAxisContactJDE (
approximateJDE , greatestJDE , direction float64 ,
) ( float64 , bool ) {
if ! finite ( approximateJDE ) || ! finite ( greatestJDE ) || direction == 0 {
return 0 , false
}
insideJDE , insideResidual := approximateJDE , solver . centralAxisEarthDiscriminant ( approximateJDE )
if ! finite ( insideResidual ) || insideResidual < 0 {
insideJDE = greatestJDE
insideResidual = solver . centralAxisEarthDiscriminant ( insideJDE )
if ! finite ( insideResidual ) || insideResidual < 0 {
return 0 , false
}
}
outsideJDE , outsideResidual := 0.0 , 0.0
foundOutside := false
for step := solarEclipseAxisContactInitialStepDays ; step <= solarEclipseAxisContactMaximumStepDays ; step *= 2 {
candidateJDE := approximateJDE + direction * step
candidateResidual := solver . centralAxisEarthDiscriminant ( candidateJDE )
if ! finite ( candidateResidual ) {
continue
}
if candidateResidual <= 0 {
outsideJDE , outsideResidual = candidateJDE , candidateResidual
foundOutside = true
break
}
insideJDE , insideResidual = candidateJDE , candidateResidual
}
if ! foundOutside {
return 0 , false
}
for iteration := 0 ; iteration < 24 && math . Abs ( outsideJDE - insideJDE ) > solarEclipseAxisContactToleranceDays ; iteration ++ {
candidateJDE := ( insideJDE + outsideJDE ) / 2
denominator := insideResidual - outsideResidual
if denominator != 0 {
fraction := insideResidual / denominator
if fraction > 0.1 && fraction < 0.9 {
candidateJDE = insideJDE + fraction * ( outsideJDE - insideJDE )
}
}
candidateResidual := solver . centralAxisEarthDiscriminant ( candidateJDE )
if ! finite ( candidateResidual ) {
return 0 , false
}
if candidateResidual >= 0 {
insideJDE , insideResidual = candidateJDE , candidateResidual
} else {
outsideJDE , outsideResidual = candidateJDE , candidateResidual
}
}
return ( insideJDE + outsideJDE ) / 2 , true
}
2026-09-23 18:55:12 +08:00
func ( solver solarEclipseSolver ) centralAxisEarthDiscriminant ( jde float64 ) float64 {
moon , axis , _ := solver . besselGeometryAt ( jde )
2026-09-17 12:27:40 +08:00
return solarEclipseLineEllipsoidDiscriminant (
moon [ 0 ], moon [ 1 ], 2 ,
moon [ 0 ], moon [ 1 ], 0 ,
solarEclipseEarthPolarRatio , 1 , axis ,
)
}
2026-09-23 18:55:12 +08:00
func ( solver solarEclipseSolver ) centralAxisContactPointAt ( jde float64 ) ( SolarEclipsePathPoint , bool ) {
moon , axis , _ := solver . besselGeometryAt ( jde )
2026-09-17 12:27:40 +08:00
cosTilt , sinTilt := math . Cos ( axis . tilt ), math . Sin ( axis . tilt )
x1 := moon [ 0 ]
y1 := cosTilt * moon [ 1 ] - 2 * sinTilt
z1 := sinTilt * moon [ 1 ] + 2 * cosTilt
x2 := moon [ 0 ]
y2 := cosTilt * moon [ 1 ]
z2 := sinTilt * moon [ 1 ]
dx , dy , dz := x2 - x1 , y2 - y1 , z2 - z1
polarRatioSquared := solarEclipseEarthPolarRatioSquared
a := dx * dx + dy * dy + dz * dz / polarRatioSquared
if ! finite ( a ) || a <= 0 {
return SolarEclipsePathPoint {}, false
}
b := x1 * dx + y1 * dy + z1 * dz / polarRatioSquared
t := - b / a
intersection := solarEclipseLineIntersection {
valid : true ,
x : x1 + dx * t ,
y : y1 + dy * t ,
z : z1 + dz * t ,
}
longitude , latitude := solarEclipseIntersectionGeodetic ( intersection , axis )
if ! finite ( longitude ) || ! finite ( latitude ) {
return SolarEclipsePathPoint {}, false
}
return SolarEclipsePathPoint {
2026-09-23 18:55:12 +08:00
JDE : jde ,
2026-09-17 12:27:40 +08:00
Longitude : longitude ,
Latitude : latitude ,
2026-10-15 23:54:15 +08:00
SunAltitude : solarEclipseSunAltitudeAtGreatest ( jde , longitude , latitude , axis . gst , solver . inst ) / rad ,
2026-09-17 12:27:40 +08:00
}, true
}
2026-09-23 18:55:12 +08:00
func ( solver solarEclipseSolver ) quickContactAt ( jde , dx , dy float64 , penumbral bool ) ( float64 , float64 , float64 , bool ) {
moon := solver . besselMoonAt ( jde )
2026-05-01 22:38:44 +08:00
radii := solver . shadowRadiiAt ( moon [ 2 ])
radius := 0.0
if penumbral {
radius = radii . penumbraRadius
}
denominator := moon [ 0 ] * moon [ 0 ] + moon [ 1 ] * moon [ 1 ]
if denominator == 0 {
return 0 , 0 , 0 , false
}
effectiveRadius := 1 - ( 1 / solarEclipseEarthPolarRatioSquared - 1 ) * moon [ 1 ] * moon [ 1 ] / denominator / 2 + radius
velocityProjection := dx * moon [ 0 ] + dy * moon [ 1 ]
if velocityProjection == 0 {
return 0 , 0 , 0 , false
}
correction := ( effectiveRadius * effectiveRadius - moon [ 0 ] * moon [ 0 ] - moon [ 1 ] * moon [ 1 ]) / ( 2 * velocityProjection )
x := moon [ 0 ] + correction * dx
y := moon [ 1 ] + correction * dy
2026-09-23 18:55:12 +08:00
jde += correction
2026-05-01 22:38:44 +08:00
curvature := ( 1 - solarEclipseEarthPolarRatioSquared ) * radius * x * y / math . Pow ( effectiveRadius , 3 )
x += curvature * y
y -= curvature * x
2026-09-23 18:55:12 +08:00
axis := solver . besselAxisAt ( jde )
2026-05-01 22:38:44 +08:00
longitude , latitude , ok := solarEclipseBesselXYToGeodetic ( x / effectiveRadius , y / effectiveRadius , axis , true )
2026-09-23 18:55:12 +08:00
return longitude , latitude , jde , ok
2026-05-01 22:38:44 +08:00
}
func ( solver solarEclipseSolver ) shadowRadiiAt ( moonBesselZ float64 ) solarEclipseShadowRadii {
return solarEclipseShadowRadii {
penumbraRadius : solver . params . penumbralK + solver . penumbraConeTangent * moonBesselZ ,
umbraRadius : solver . params . umbralK - solver . umbraConeTangent * moonBesselZ ,
absUmbraRadius : math . Abs ( solver . params . umbralK - solver . umbraConeTangent * moonBesselZ ),
2026-09-23 18:55:12 +08:00
magnitude : solver . params . umbralK / moonBesselZ / solver . params . sunRadiusRatio * ( solver . meanSunMoonDistance + moonBesselZ ),
2026-05-01 22:38:44 +08:00
}
}
2026-09-23 18:55:12 +08:00
func ( solver solarEclipseSolver ) besselAxisAt ( jde float64 ) solarEclipseAxis {
2026-10-15 23:54:15 +08:00
sun , moon := solarEclipseSunMoonEquatorial ( jde , solver . inst )
2026-09-17 12:27:40 +08:00
return solarEclipseBesselAxisFromEquatorialWithDeltaT (
2026-09-23 18:55:12 +08:00
jde , sun , moon , solver . effectiveDeltaTSeconds ( jde ),
2026-09-17 12:27:40 +08:00
)
}
func solarEclipseBesselAxisFromEquatorial ( jd float64 , sun , moon [ 3 ] float64 ) solarEclipseAxis {
return solarEclipseBesselAxisFromEquatorialWithDeltaT ( jd , sun , moon , DeltaT ( jd , true ))
}
// solarEclipseBesselAxisFromEquatorialWithDeltaT 用显式 ΔT 构造贝塞尔轴:TT 时刻保持
// 不变,ΔT 只决定地球自转相位(恒星时),因此同一 TT 在不同 ΔT 下得到的地面足迹会
// 沿经度平移,这正是"ΔT 只影响自转、不影响几何时刻"的实现点。
// solarEclipseBesselAxisFromEquatorialWithDeltaT builds the Besselian axis with an
// explicit ΔT: the TT instant is untouched and ΔT only sets Earth rotation.
func solarEclipseBesselAxisFromEquatorialWithDeltaT (
jd float64 , sun , moon [ 3 ] float64 , deltaTSeconds float64 ,
) solarEclipseAxis {
2026-05-01 22:38:44 +08:00
sunXYZ := solarEclipseLLRToXYZ ( sun [ 0 ], sun [ 1 ], sun [ 2 ])
moonXYZ := solarEclipseLLRToXYZ ( moon [ 0 ], moon [ 1 ], moon [ 2 ])
axis := solarEclipseXYZToLLR ( sunXYZ [ 0 ] - moonXYZ [ 0 ], sunXYZ [ 1 ] - moonXYZ [ 1 ], sunXYZ [ 2 ] - moonXYZ [ 2 ])
2026-09-17 12:27:40 +08:00
utJDE := jd - deltaTSeconds / 86400
2026-05-01 22:38:44 +08:00
return solarEclipseAxis {
rightAscension : solarEclipseNormalizeRadians ( math . Pi / 2 + axis [ 0 ]),
tilt : math . Pi / 2 - axis [ 1 ],
2026-10-15 23:54:15 +08:00
gst : normalizeSignedRadians ( ApparentSiderealTime ( utJDE ) * 15 * rad ),
2026-05-01 22:38:44 +08:00
}
}
2026-09-23 18:55:12 +08:00
func ( solver solarEclipseSolver ) besselMoonAt ( jde float64 ) [ 3 ] float64 {
moon , _ , _ := solver . besselGeometryAt ( jde )
2026-09-17 12:27:40 +08:00
return moon
}
2026-09-23 18:55:12 +08:00
func ( solver solarEclipseSolver ) besselMoonCandidateAt ( jde float64 ) [ 3 ] float64 {
moon , _ , _ , ok := solver . besselGeometryCandidateAt ( jde )
2026-09-17 12:27:40 +08:00
if ! ok {
2026-09-23 18:55:12 +08:00
return solver . besselMoonAt ( jde )
2026-09-17 12:27:40 +08:00
}
return moon
}
2026-05-01 22:38:44 +08:00
2026-09-23 18:55:12 +08:00
func ( solver solarEclipseSolver ) besselGeometryAt ( jde float64 ) ([ 3 ] float64 , solarEclipseAxis , [ 3 ] float64 ) {
key := math . Float64bits ( jde )
2026-09-17 12:27:40 +08:00
// 命中要求 ΔT 世代一致:轴里的 gst 依赖 ΔT,SetDeltaTFn 之后旧条目必须视为未命中。
// A hit requires the same ΔT generation: the cached axis carries a ΔT-dependent gst,
// so entries written before a SetDeltaTFn override must count as misses.
if entry , ok := solver . besselGeometryCache [ key ]; ok && entry . generation == deltaTGenerationValue () {
return entry . moon , entry . axis , entry . sun
}
2026-10-15 23:54:15 +08:00
sun , moon := solarEclipseSunMoonEquatorial ( jde , solver . inst )
2026-09-17 12:27:40 +08:00
axis := solarEclipseBesselAxisFromEquatorialWithDeltaT (
2026-09-23 18:55:12 +08:00
jde , sun , moon , solver . effectiveDeltaTSeconds ( jde ),
2026-09-17 12:27:40 +08:00
)
geometry := solarEclipseBesselGeometryCacheEntry {
moon : solarEclipseBesselMoonFromEquatorial ( moon , axis ),
axis : axis ,
sun : sun ,
valid : true ,
}
storeSolarEclipseBesselGeometry ( solver . besselGeometryCache , key , geometry )
return geometry . moon , geometry . axis , geometry . sun
}
2026-09-23 18:55:12 +08:00
func ( solver solarEclipseSolver ) besselGeometryCandidateAt ( jde float64 ) ([ 3 ] float64 , solarEclipseAxis , [ 3 ] float64 , bool ) {
key := math . Float64bits ( jde )
2026-09-17 12:27:40 +08:00
if entry , ok := solver . besselCandidateCache [ key ]; ok && entry . generation == deltaTGenerationValue () {
return entry . moon , entry . axis , entry . sun , entry . valid
}
if solver . localEphemeris == nil {
return [ 3 ] float64 {}, solarEclipseAxis {}, [ 3 ] float64 {}, false
}
2026-09-23 18:55:12 +08:00
sun , moon , ok := solver . localEphemeris . equatorialAt ( jde )
2026-09-17 12:27:40 +08:00
if ! ok {
return [ 3 ] float64 {}, solarEclipseAxis {}, [ 3 ] float64 {}, false
}
axis := solarEclipseBesselAxisFromEquatorialWithDeltaT (
2026-09-23 18:55:12 +08:00
jde , sun , moon , solver . effectiveDeltaTSeconds ( jde ),
2026-09-17 12:27:40 +08:00
)
geometry := solarEclipseBesselGeometryCacheEntry {
moon : solarEclipseBesselMoonFromEquatorial ( moon , axis ),
axis : axis ,
sun : sun ,
valid : true ,
}
storeSolarEclipseBesselGeometry ( solver . besselCandidateCache , key , geometry )
return geometry . moon , geometry . axis , geometry . sun , geometry . valid
}
func storeSolarEclipseBesselGeometry (
cache map [ uint64 ] solarEclipseBesselGeometryCacheEntry ,
key uint64 ,
entry solarEclipseBesselGeometryCacheEntry ,
) {
if cache == nil {
return
}
if _ , exists := cache [ key ]; ! exists && len ( cache ) >= solarEclipseBesselGeometryCacheMaximumEntries {
for cachedKey := range cache {
delete ( cache , cachedKey )
}
}
entry . generation = deltaTGenerationValue ()
cache [ key ] = entry
}
func solarEclipseBesselMoonFromEquatorial ( moon [ 3 ] float64 , axis solarEclipseAxis ) [ 3 ] float64 {
2026-05-01 22:38:44 +08:00
rotated := solarEclipseRotateLLR (
2026-10-15 23:54:15 +08:00
normalizeSignedRadians ( moon [ 0 ] - axis . rightAscension ),
2026-05-01 22:38:44 +08:00
moon [ 1 ],
moon [ 2 ],
- axis . tilt ,
)
rectangular := solarEclipseLLRToXYZ ( rotated [ 0 ], rotated [ 1 ], rotated [ 2 ])
return [ 3 ] float64 {
2026-10-15 23:54:15 +08:00
rectangular [ 0 ] / geodata . EarthEquatorialRadiusKM ,
rectangular [ 1 ] / geodata . EarthEquatorialRadiusKM ,
rectangular [ 2 ] / geodata . EarthEquatorialRadiusKM ,
2026-05-01 22:38:44 +08:00
}
}
2026-10-15 23:54:15 +08:00
func solarEclipseSunMoonEquatorial ( jde float64 , instances ...* Instance ) ([ 3 ] float64 , [ 3 ] float64 ) {
inst := instanceValue ( instances )
2026-09-23 18:55:12 +08:00
julianCentury := ( jde - 2451545.0 ) / 36525.0
nutationLongitude , nutationObliquity := Nutation2000B ( jde )
obliquity := ( Obliquity1980 ( jde ) + nutationObliquity ) * rad
2026-05-01 22:38:44 +08:00
2026-09-17 12:27:40 +08:00
// Share the full-series distance and nutation for this single TT.
2026-10-15 23:54:15 +08:00
sunDistanceAU := EarthAwayWithInstance ( jde , inst )
// 新模型全精度时 HSunApparentLoN 已含光行差,避免重复计入常数项。
sunLongitude := HSunApparentLoNWithInstance ( jde , - 1 , inst ) * rad
sunLatitude := HSunTrueBoWithInstance ( jde , inst ) * rad
2026-09-17 12:27:40 +08:00
sunDistance := sunDistanceAU * solarEclipseAstronomicalUnitKM
2026-05-01 22:38:44 +08:00
2026-10-15 23:54:15 +08:00
// 新模型全精度时 HMoonApparentLoN 已含光行差,避免重复计入常数项。
moonLongitude := solarEclipseNormalizeRadians ( HMoonApparentLoNWithInstance ( jde , - 1 , inst ) * rad )
moonLatitude := HMoonTrueBoWithInstance ( jde , inst ) * rad
if instanceModel ( inst ) == ephem . Legacy {
sunLongitude = ( HSunTrueLoNWithInstance ( jde , - 1 , inst ) + nutationLongitude - 20.49552 / sunDistanceAU / 3600 ) * rad
moonLongitude = solarEclipseNormalizeRadians (( HMoonTrueLoNWithInstance ( jde , - 1 , inst ) + nutationLongitude ) * rad + solarEclipseMoonLonAberrRad )
moonLatitude += moonLatitudeAberrationRad ( julianCentury )
}
moonDistance := HMoonAwayWithInstance ( jde , inst )
2026-05-01 22:38:44 +08:00
sunEquatorial := solarEclipseRotateLLR ( sunLongitude , sunLatitude , sunDistance , obliquity )
moonEquatorial := solarEclipseRotateLLR ( moonLongitude , moonLatitude , moonDistance , obliquity )
return [ 3 ] float64 { sunEquatorial [ 0 ], sunEquatorial [ 1 ], sunEquatorial [ 2 ]},
[ 3 ] float64 { moonEquatorial [ 0 ], moonEquatorial [ 1 ], moonEquatorial [ 2 ]}
}
2026-10-15 23:54:15 +08:00
func solarEclipseSunAltitudeAtGreatest ( jde , lonDeg , latDeg , gst float64 , instances ...* Instance ) float64 {
inst := instanceValue ( instances )
sun , _ := solarEclipseSunMoonEquatorial ( jde , inst )
2026-09-17 12:27:40 +08:00
return solarEclipseSunAltitudeFromEquatorial ( sun , lonDeg , latDeg , gst )
}
func solarEclipseSunAltitudeFromEquatorial ( sun [ 3 ] float64 , lonDeg , latDeg , gst float64 ) float64 {
2026-05-01 22:38:44 +08:00
horizon := solarEclipseEquatorialToHorizontal ( sun [ 0 ], sun [ 1 ], sun [ 2 ], lonDeg * rad , latDeg * rad , gst )
return horizon [ 1 ]
}
func solarEclipseEquatorialToHorizontal ( ra , dec , distance , lon , lat , gst float64 ) [ 3 ] float64 {
rotated := solarEclipseRotateLLR (
solarEclipseNormalizeRadians ( ra + math . Pi / 2 - gst - lon ),
dec ,
distance ,
math . Pi / 2 - lat ,
)
return [ 3 ] float64 {
solarEclipseNormalizeRadians ( math . Pi / 2 - rotated [ 0 ]),
rotated [ 1 ],
rotated [ 2 ],
}
}
func solarEclipseLLRToXYZ ( longitude , latitude , distance float64 ) [ 3 ] float64 {
return [ 3 ] float64 {
distance * math . Cos ( latitude ) * math . Cos ( longitude ),
distance * math . Cos ( latitude ) * math . Sin ( longitude ),
distance * math . Sin ( latitude ),
}
}
func solarEclipseXYZToLLR ( x , y , z float64 ) [ 3 ] float64 {
distance := math . Sqrt ( x * x + y * y + z * z )
return [ 3 ] float64 {
solarEclipseNormalizeRadians ( math . Atan2 ( y , x )),
math . Asin ( z / distance ),
distance ,
}
}
func solarEclipseRotateLLR ( longitude , latitude , distance , obliquity float64 ) [ 3 ] float64 {
rotatedLongitude := math . Atan2 (
math . Sin ( longitude ) * math . Cos ( obliquity ) - math . Tan ( latitude ) * math . Sin ( obliquity ),
math . Cos ( longitude ),
)
return [ 3 ] float64 {
solarEclipseNormalizeRadians ( rotatedLongitude ),
math . Asin ( math . Cos ( obliquity ) * math . Sin ( latitude ) + math . Sin ( obliquity ) * math . Cos ( latitude ) * math . Sin ( longitude )),
distance ,
}
}
func solarEclipseLineEar2 ( x1 , y1 , z1 , x2 , y2 , z2 , polarRatio , radius float64 , axis solarEclipseAxis ) solarEclipseLineIntersection {
cosTilt := math . Cos ( axis . tilt )
sinTilt := math . Sin ( axis . tilt )
x1Rot := x1
y1Rot := cosTilt * y1 - sinTilt * z1
z1Rot := sinTilt * y1 + cosTilt * z1
x2Rot := x2
y2Rot := cosTilt * y2 - sinTilt * z2
z2Rot := sinTilt * y2 + cosTilt * z2
intersection := solarEclipseLineEllipsoid ( x1Rot , y1Rot , z1Rot , x2Rot , y2Rot , z2Rot , polarRatio , radius )
if ! intersection . valid {
return intersection
}
return intersection
}
func solarEclipseLineEllipsoid ( x1 , y1 , z1 , x2 , y2 , z2 , polarRatio , radius float64 ) solarEclipseLineIntersection {
dx := x2 - x1
dy := y2 - y1
dz := z2 - z1
polarRatioSquared := polarRatio * polarRatio
a := dx * dx + dy * dy + dz * dz / polarRatioSquared
b := x1 * dx + y1 * dy + z1 * dz / polarRatioSquared
c := x1 * x1 + y1 * y1 + z1 * z1 / polarRatioSquared - radius * radius
discriminant := b * b - a * c
if discriminant < 0 {
return solarEclipseLineIntersection {}
}
root := math . Sqrt ( discriminant )
if b < 0 {
root = - root
}
t := ( - b + root ) / a
x := x1 + dx * t
y := y1 + dy * t
z := z1 + dz * t
distance := math . Sqrt ( dx * dx + dy * dy + dz * dz )
return solarEclipseLineIntersection {
valid : true ,
x : x ,
y : y ,
z : z ,
r1 : distance * math . Abs ( t ),
r2 : distance * math . Abs ( t - 1 ),
}
}
func axisIntersectionLongitude ( intersection solarEclipseLineIntersection , axis solarEclipseAxis ) float64 {
longitude , _ := solarEclipseIntersectionGeodetic ( intersection , axis )
return longitude
}
func axisIntersectionLatitude ( intersection solarEclipseLineIntersection , axis solarEclipseAxis ) float64 {
_ , latitude := solarEclipseIntersectionGeodetic ( intersection , axis )
return latitude
}
func solarEclipseIntersectionGeodetic ( intersection solarEclipseLineIntersection , axis solarEclipseAxis ) ( float64 , float64 ) {
2026-10-15 23:54:15 +08:00
longitude := normalizeSignedRadians ( math . Atan2 ( intersection . y , intersection . x ) + axis . rightAscension - axis . gst )
2026-05-01 22:38:44 +08:00
latitude := math . Atan ( intersection . z / solarEclipseEarthPolarRatioSquared / math . Sqrt ( intersection . x * intersection . x + intersection . y * intersection . y ))
return longitude * deg , latitude * deg
}
func solarEclipseBesselPointToGeodetic ( x , y , z float64 , axis solarEclipseAxis , ellipsoidal bool ) ( float64 , float64 ) {
point := solarEclipseXYZToLLR ( x , y , z )
rotated := solarEclipseRotateLLR ( point [ 0 ], point [ 1 ], point [ 2 ], axis . tilt )
2026-10-15 23:54:15 +08:00
longitude := normalizeSignedRadians ( rotated [ 0 ] + axis . rightAscension - axis . gst )
2026-05-01 22:38:44 +08:00
latitude := rotated [ 1 ]
if ellipsoidal {
latitude = math . Atan ( math . Tan ( latitude ) / solarEclipseEarthPolarRatioSquared )
}
return longitude * deg , latitude * deg
}
func solarEclipseBesselXYToGeodetic ( x , y float64 , axis solarEclipseAxis , ellipsoidal bool ) ( float64 , float64 , bool ) {
polarRatio := 1.0
if ellipsoidal {
polarRatio = solarEclipseEarthPolarRatio
}
intersection := solarEclipseLineEar2 ( x , y , 2 , x , y , 0 , polarRatio , 1 , axis )
if ! intersection . valid {
return 0 , 0 , false
}
longitude , latitude := solarEclipseIntersectionGeodetic ( intersection , axis )
return longitude , latitude , true
}
func solarEclipseNormalizeRadians ( angle float64 ) float64 {
angle = math . Mod ( angle , 2 * math . Pi )
if angle < 0 {
angle += 2 * math . Pi
}
return angle
}
2026-10-15 23:54:15 +08:00
func normalizeSignedRadians ( angle float64 ) float64 {
2026-05-01 22:38:44 +08:00
angle = math . Mod ( angle , 2 * math . Pi )
if angle <= - math . Pi {
angle += 2 * math . Pi
}
if angle > math . Pi {
angle -= 2 * math . Pi
}
return angle
}