package basic import "math" // 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" ) // 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 } // SolarEclipseType 整场日食的全局食型。 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 { // 下列字段是决定上述数值的口径,随结果一起保留。 // The fields below are the conventions that fix the numbers above. Model SolarEclipseRadiusModel SunRadiusModel SolarEclipseSunRadiusModel Type SolarEclipseType Centrality SolarEclipseCentrality // GreatestEclipse 是全局“影轴最接近地心”的时刻。 GreatestEclipse float64 // PartialBeginOnEarth / PartialEndOnEarth 是地球范围的偏食开始 / 结束时刻。 PartialBeginOnEarth float64 PartialEndOnEarth float64 // CentralBeginOnEarth / CentralEndOnEarth 是中心线进入 / 离开地球的时刻。 CentralBeginOnEarth float64 CentralEndOnEarth float64 // Magnitude 是全局食分。 Magnitude float64 // Gamma 是月影轴到地心的有符号最小距离,单位为地球赤道半径。 Gamma float64 // 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 // 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. PathWidthKM float64 // 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 // GreatestLongitude / GreatestLatitude 是日食食甚点地理坐标,东经为正,西经为负。 GreatestLongitude float64 GreatestLatitude float64 HasPartial bool HasCentral bool HasAnnular bool HasTotal bool HasHybrid bool } type solarEclipseModelParameters struct { penumbralK float64 umbralK float64 sunRadiusRatio float64 } type solarEclipseShadowRadii struct { penumbraRadius float64 umbraRadius float64 absUmbraRadius float64 magnitude float64 } type solarEclipseAxis struct { rightAscension float64 tilt float64 gst float64 } type solarEclipseSolver struct { newMoonJDE float64 model SolarEclipseRadiusModel sunRadiusModel SolarEclipseSunRadiusModel params solarEclipseModelParameters localStateContextCache map[uint64]localSolarEclipseStateContext localEphemeris *solarEclipseLocalEphemeris // deltaTSeconds 是调用方显式给出的 ΔT(秒);NaN 表示未覆盖,用进程级模型。 // 只影响地球自转相位(轴的 gst),不改变任何 TT 时刻。 deltaTSeconds float64 besselGeometryCache map[uint64]solarEclipseBesselGeometryCacheEntry besselCandidateCache map[uint64]solarEclipseBesselGeometryCacheEntry exactCentralContact bool meanSunMoonDistance float64 penumbraConeTangent float64 umbraConeTangent float64 } 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 } 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 ( solarEclipseEarthEquatorialRadiusKM = 6378.1366 // 赤道自转线速度,用于把 Δ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 // 标准档与边缘档在 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 solarEclipseUmbralK = 0.2722810 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 solarEclipseNodeCount = 7 solarEclipseNodeStepDays = 0.04 solarEclipseMoonLonAberrRad = -3.4e-6 solarEclipseAxisContactInitialStepDays = 1.0 / 86400.0 solarEclipseAxisContactMaximumStepDays = 30.0 / 1440.0 solarEclipseAxisContactToleranceDays = 1e-9 // 这两个系数沿用经典贝塞尔近似中的极区有效半径经验值。 solarEclipseNonCentralLimit = 0.9972 solarEclipseCentralLimit = 0.9966 ) var solarEclipseArcsecPerRadian = 180.0 * 3600.0 / math.Pi 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 } // SolarEclipseSunSemidiameter 指定太阳半径口径下的视半径,单位角秒 / apparent solar semidiameter in arcseconds under a given eclipse sun radius convention. func SolarEclipseSunSemidiameter(jde float64, model SolarEclipseSunRadiusModel) float64 { return angularSemidiameterFromAU(solarEclipseSunRadiusRatio(model)*solarEclipseEarthEquatorialRadiusKM, EarthAwayN(jde, -1)) } // SolarEclipse 计算给定近朔时刻附近的一次全局日食,默认使用 NASABulletin Split-K 模型与标准太阳半径。 func SolarEclipse(seedJDE float64) SolarEclipseResult { return SolarEclipseNASABulletinSplitK(seedJDE) } // SolarEclipseWithOptions 计算给定近朔时刻附近的一次全局日食,半径口径由 options 指定 / computes one global solar eclipse with the given radius conventions. func SolarEclipseWithOptions(seedJDE float64, options SolarEclipseOptions) SolarEclipseResult { return solarEclipseWithDeltaT(seedJDE, options, 0) } // SolarEclipseIAUSingleK 计算给定近朔时刻附近的一次全局日食,使用 IAU Single-K 模型。 func SolarEclipseIAUSingleK(seedJDE float64) SolarEclipseResult { return solarEclipse(seedJDE, SolarEclipseModelIAUSingleK) } // SolarEclipseNASABulletinSplitK 计算给定近朔时刻附近的一次全局日食,使用 NASA bulletin Split-K 模型。 func SolarEclipseNASABulletinSplitK(seedJDE float64) SolarEclipseResult { return solarEclipse(seedJDE, SolarEclipseModelNASABulletinSplitK) } func solarEclipse(seedJDE float64, model SolarEclipseRadiusModel) SolarEclipseResult { return solarEclipseWithDeltaT(seedJDE, SolarEclipseOptions{RadiusModel: model}, 0) } func solarEclipseWithDeltaT( seedJDE float64, options SolarEclipseOptions, deltaTSeconds float64, ) SolarEclipseResult { newMoonJDE := CalcMoonSHByJDE(seedJDE, 0) solver := newSolarEclipseSolverWithOptions(newMoonJDE, options).withDeltaTSeconds(deltaTSeconds) return solver.eclipseResult() } func (solver solarEclipseSolver) eclipseResult() SolarEclipseResult { model := solver.model feature := solver.feature() result := SolarEclipseResult{ Model: model, SunRadiusModel: solver.sunRadiusModel, 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 } if solarEclipseTwoLimitsTypeCode(feature.typeCode) { result.Centrality = SolarEclipseCentralTwoLimits } else if feature.typeCode == "A1" || feature.typeCode == "T1" { result.Centrality = SolarEclipseCentralOneLimit } result.PathWidthDefined = result.Centrality == SolarEclipseCentralTwoLimits 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 result.CentralDurationDays = solver.greatestCentralDuration(result) } 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 } // 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, ) } func newSolarEclipseSolver(newMoonJDE float64, model SolarEclipseRadiusModel) solarEclipseSolver { return newSolarEclipseSolverWithOptions(newMoonJDE, SolarEclipseOptions{RadiusModel: model}) } func newSolarEclipseSolverWithOptions(newMoonJDE float64, options SolarEclipseOptions) solarEclipseSolver { options.RadiusModel = normalizeSolarEclipseRadiusModel(options.RadiusModel) options.SunRadiusModel = normalizeSolarEclipseSunRadiusModel(options.SunRadiusModel) params := solarEclipseModelParams(options.RadiusModel, options.SunRadiusModel) firstNodeJDE := newMoonJDE + (0-float64(solarEclipseNodeCount)/2+0.5)*solarEclipseNodeStepDays lastNodeJDE := newMoonJDE + (float64(solarEclipseNodeCount-1)-float64(solarEclipseNodeCount)/2+0.5)*solarEclipseNodeStepDays firstSun, firstMoon := solarEclipseSunMoonEquatorial(firstNodeJDE) lastSun, lastMoon := solarEclipseSunMoonEquatorial(lastNodeJDE) meanSunMoonDistance := ((firstSun[2] + lastSun[2]) - (firstMoon[2] + lastMoon[2])) / 2 / solarEclipseEarthEquatorialRadiusKM return solarEclipseSolver{ newMoonJDE: newMoonJDE, model: options.RadiusModel, sunRadiusModel: options.SunRadiusModel, params: params, deltaTSeconds: math.NaN(), localStateContextCache: make(map[uint64]localSolarEclipseStateContext), besselGeometryCache: make(map[uint64]solarEclipseBesselGeometryCacheEntry), besselCandidateCache: make(map[uint64]solarEclipseBesselGeometryCacheEntry), meanSunMoonDistance: meanSunMoonDistance, penumbraConeTangent: (params.sunRadiusRatio + params.penumbralK) / meanSunMoonDistance, umbraConeTangent: (params.sunRadiusRatio - params.umbralK) / meanSunMoonDistance, } } // 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(秒)。 // 未覆盖时用真 TT−UT1(观测表/外推),不能回退到混入 UTC 的进程级 DeltaT, // 否则恒星时相位会少掉 DUT1,站心与影轴两条路径就不一致。 func (solver solarEclipseSolver) effectiveDeltaTSeconds(jde float64) float64 { if math.IsNaN(solver.deltaTSeconds) { return ut1ToTTOffsetSeconds(ttToUT1JDE(jde)) } return solver.deltaTSeconds } // siderealTimeAt 返回某 TT 时刻的视恒星时(弧度),ΔT 覆盖时同样生效。 func (solver solarEclipseSolver) siderealTimeAt(jde float64) float64 { ut1JDE := TT2UT1(jde) if !math.IsNaN(solver.deltaTSeconds) { ut1JDE = jde - solver.deltaTSeconds/86400 } return ApparentSiderealTime(ut1JDE) * 15 * rad } // 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 { solver.localEphemeris = newSolarEclipseLocalEphemeris(solver.newMoonJDE) } return solver } // solarEclipseTwoLimitsTypeCode 报告该类型码的南北两限是否都存在,带宽解析式只在这一类中心食上有定义。 func solarEclipseTwoLimitsTypeCode(typeCode string) bool { switch typeCode { case "A", "T", "H", "H2", "H3": return true } return false } func (solver solarEclipseSolver) feature() solarEclipseFeature { const finiteDifferenceStep = 0.04 candidateSolver := solver.withLocalEphemeris() jde := solver.newMoonJDE before := candidateSolver.besselMoonCandidateAt(jde - finiteDifferenceStep) center := candidateSolver.besselMoonCandidateAt(jde) after := candidateSolver.besselMoonCandidateAt(jde + finiteDifferenceStep) 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 greatestEclipseJDE := jde + t0 // 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] 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" } } // 单侧极限(A1/T1)只有一侧限界,非中心中心食(A0/T0)连限界都没有:解析式 2r/|sin h| // 在 h→0 时发散,两类事件该栏都无定义,与中心线逐点宽度一起置 0。 if solarEclipseTwoLimitsTypeCode(typeCode) { sunAltitude := solarEclipseSunAltitudeAtGreatest(greatestEclipseJDE, greatestLongitude, greatestLatitude, axis.gst) if math.Abs(math.Sin(sunAltitude)) > 1e-12 { pathWidthKM = math.Abs(2*greatestRadii.umbraRadius*solarEclipseEarthEquatorialRadiusKM) / math.Abs(math.Sin(sunAltitude)) } } } feature := solarEclipseFeature{ greatestEclipseJDE: greatestEclipseJDE, greatestLongitude: greatestLongitude, greatestLatitude: greatestLatitude, magnitude: magnitude, gamma: gamma, pathWidthKM: pathWidthKM, typeCode: typeCode, } if typeCode != "N" { _, _, feature.partialBeginJDE, _ = solver.quickContactAt(partialStartParam+jde, vx, vy, true) _, _, feature.partialEndJDE, _ = solver.quickContactAt(partialEndParam+jde, vx, vy, true) } if axisIntersection.valid && typeCode != "N" && typeCode != "P" { _, _, feature.centralBeginJDE, _ = solver.quickContactAt(centralStartParam+jde, vx, vy, false) _, _, feature.centralEndJDE, _ = solver.quickContactAt(centralEndParam+jde, vx, vy, false) 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 } } return feature } 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 } func (solver solarEclipseSolver) centralAxisEarthDiscriminant(jde float64) float64 { moon, axis, _ := solver.besselGeometryAt(jde) return solarEclipseLineEllipsoidDiscriminant( moon[0], moon[1], 2, moon[0], moon[1], 0, solarEclipseEarthPolarRatio, 1, axis, ) } func (solver solarEclipseSolver) centralAxisContactPointAt(jde float64) (SolarEclipsePathPoint, bool) { moon, axis, _ := solver.besselGeometryAt(jde) 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{ JDE: jde, Longitude: longitude, Latitude: latitude, SunAltitude: solarEclipseSunAltitudeAtGreatest(jde, longitude, latitude, axis.gst) / rad, }, true } func (solver solarEclipseSolver) quickContactAt(jde, dx, dy float64, penumbral bool) (float64, float64, float64, bool) { moon := solver.besselMoonAt(jde) 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 jde += correction curvature := (1 - solarEclipseEarthPolarRatioSquared) * radius * x * y / math.Pow(effectiveRadius, 3) x += curvature * y y -= curvature * x axis := solver.besselAxisAt(jde) longitude, latitude, ok := solarEclipseBesselXYToGeodetic(x/effectiveRadius, y/effectiveRadius, axis, true) return longitude, latitude, jde, ok } 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), magnitude: solver.params.umbralK / moonBesselZ / solver.params.sunRadiusRatio * (solver.meanSunMoonDistance + moonBesselZ), } } func (solver solarEclipseSolver) besselAxisAt(jde float64) solarEclipseAxis { sun, moon := solarEclipseSunMoonEquatorial(jde) return solarEclipseBesselAxisFromEquatorialWithDeltaT( jde, sun, moon, solver.effectiveDeltaTSeconds(jde), ) } 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 { 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]) utJDE := jd - deltaTSeconds/86400 return solarEclipseAxis{ rightAscension: solarEclipseNormalizeRadians(math.Pi/2 + axis[0]), tilt: math.Pi/2 - axis[1], gst: solarEclipseNormalizeSignedRadians(ApparentSiderealTime(utJDE) * 15 * rad), } } func (solver solarEclipseSolver) besselMoonAt(jde float64) [3]float64 { moon, _, _ := solver.besselGeometryAt(jde) return moon } func (solver solarEclipseSolver) besselMoonCandidateAt(jde float64) [3]float64 { moon, _, _, ok := solver.besselGeometryCandidateAt(jde) if !ok { return solver.besselMoonAt(jde) } return moon } func (solver solarEclipseSolver) besselGeometryAt(jde float64) ([3]float64, solarEclipseAxis, [3]float64) { key := math.Float64bits(jde) // 命中要求 Δ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 } sun, moon := solarEclipseSunMoonEquatorial(jde) axis := solarEclipseBesselAxisFromEquatorialWithDeltaT( jde, sun, moon, solver.effectiveDeltaTSeconds(jde), ) geometry := solarEclipseBesselGeometryCacheEntry{ moon: solarEclipseBesselMoonFromEquatorial(moon, axis), axis: axis, sun: sun, valid: true, } storeSolarEclipseBesselGeometry(solver.besselGeometryCache, key, geometry) return geometry.moon, geometry.axis, geometry.sun } func (solver solarEclipseSolver) besselGeometryCandidateAt(jde float64) ([3]float64, solarEclipseAxis, [3]float64, bool) { key := math.Float64bits(jde) 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 } sun, moon, ok := solver.localEphemeris.equatorialAt(jde) if !ok { return [3]float64{}, solarEclipseAxis{}, [3]float64{}, false } axis := solarEclipseBesselAxisFromEquatorialWithDeltaT( jde, sun, moon, solver.effectiveDeltaTSeconds(jde), ) 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 { rotated := solarEclipseRotateLLR( solarEclipseNormalizeSignedRadians(moon[0]-axis.rightAscension), moon[1], moon[2], -axis.tilt, ) rectangular := solarEclipseLLRToXYZ(rotated[0], rotated[1], rotated[2]) return [3]float64{ rectangular[0] / solarEclipseEarthEquatorialRadiusKM, rectangular[1] / solarEclipseEarthEquatorialRadiusKM, rectangular[2] / solarEclipseEarthEquatorialRadiusKM, } } func solarEclipseSunMoonEquatorial(jde float64) ([3]float64, [3]float64) { julianCentury := (jde - 2451545.0) / 36525.0 nutationLongitude, nutationObliquity := Nutation2000B(jde) obliquity := (Obliquity1980(jde) + nutationObliquity) * rad // Share the full-series distance and nutation for this single TT. sunDistanceAU := EarthAway(jde) sunLongitude := (HSunTrueLoN(jde, -1) + nutationLongitude - 20.49552/sunDistanceAU/3600) * rad sunLatitude := HSunTrueBo(jde) * rad sunDistance := sunDistanceAU * solarEclipseAstronomicalUnitKM moonLongitude := solarEclipseNormalizeRadians((HMoonTrueLoN(jde, -1)+nutationLongitude)*rad + solarEclipseMoonLonAberrRad) moonLatitude := HMoonTrueBo(jde)*rad + moonLatitudeAberrationRad(julianCentury) moonDistance := HMoonAway(jde) 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]} } func solarEclipseSunAltitudeAtGreatest(jde, lonDeg, latDeg, gst float64) float64 { sun, _ := solarEclipseSunMoonEquatorial(jde) return solarEclipseSunAltitudeFromEquatorial(sun, lonDeg, latDeg, gst) } func solarEclipseSunAltitudeFromEquatorial(sun [3]float64, lonDeg, latDeg, gst float64) float64 { 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) { longitude := solarEclipseNormalizeSignedRadians(math.Atan2(intersection.y, intersection.x) + axis.rightAscension - axis.gst) 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) longitude := solarEclipseNormalizeSignedRadians(rotated[0] + axis.rightAscension - axis.gst) 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 } func solarEclipseNormalizeSignedRadians(angle float64) float64 { 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 }