package basic import ( "math" "b612.me/astro/internal/geodata" ) // SkyBody 通用天象搜索支持的天体 / body for generic sky event searches. type SkyBody int const ( // SkySun 太阳 / the Sun. SkySun SkyBody = iota // SkyMoon 月球 / the Moon. SkyMoon // SkyMercury 水星 / Mercury. SkyMercury // SkyVenus 金星 / Venus. SkyVenus // SkyMars 火星 / Mars. SkyMars // SkyJupiter 木星 / Jupiter. SkyJupiter // SkySaturn 土星 / Saturn. SkySaturn // SkyUranus 天王星 / Uranus. SkyUranus // SkyNeptune 海王星 / Neptune. SkyNeptune ) // SkyEvent 通用天象事件:TT 儒略日与事件时的角量(度)/ generic sky event at TT JDE with its angle in degrees. type SkyEvent struct { JDE float64 Value float64 } func (body SkyBody) valid() bool { return body >= SkySun && body <= SkyNeptune } // skyScanStep 保证相邻采样间视黄经变化远小于 180°,逆行回转也不会漏根。 func skyScanStep(bodies ...SkyBody) float64 { step := 2.0 for _, b := range bodies { switch b { case SkyMoon: step = math.Min(step, 0.25) case SkyMercury, SkyVenus, SkySun: step = math.Min(step, 1.0) } } return step } // SkyBodyApparentLoBoWithInstance 地心视黄经、视黄纬(度)/ geocentric apparent ecliptic longitude and latitude. func SkyBodyApparentLoBoWithInstance(body SkyBody, jde float64, inst *Instance) (float64, float64) { switch body { case SkySun: return sunApparentLoBoN(jde, -1, inst) case SkyMoon: return moonApparentLoBoN(jde, -1, inst) case SkyMercury, SkyVenus, SkyMars, SkyJupiter, SkySaturn, SkyUranus, SkyNeptune: return planetApparentLoBoWithInstance(int(body-SkyMercury)+1, jde, inst) } return math.NaN(), math.NaN() } // skyGridMaxIntervals 网格段数上限;须能放进 32 位 int,超出时扫描本身也不可能完成。 const skyGridMaxIntervals = 1 << 30 // skyGrid 把 [start, end] 均分为至少 minIntervals 段、每段不超过 step;按下标取点,浮点下必然前进且末点恰为 end。 func skyGrid(start, end, step float64, minIntervals int) (count int, at func(int) float64, ok bool) { span := end - start if !finite(start) || !finite(end) || !(span > 0) || !(step > 0) { return 0, nil, false } ratio := math.Ceil(span / step) if !(ratio < skyGridMaxIntervals) { return 0, nil, false } count = minIntervals if ratio > float64(count) { count = int(ratio) } return count, func(index int) float64 { if index == count { return end } return start + span*float64(index)/float64(count) }, true } // skyRoots 在闭区间 [start, end] 内按 step 扫描 f 的零点并二分到约 0.1 秒;f 须连续(已处理 360° 回绕)。 func skyRoots(start, end, step float64, f func(float64) float64) []float64 { count, at, ok := skyGrid(start, end, step, 1) if !ok { return nil } var roots []float64 prevT, prev := start, f(start) if prev == 0 { roots = append(roots, start) } for index := 1; index <= count; index++ { t := at(index) cur := f(t) switch { case cur == 0: roots = append(roots, t) // 回绕跳变时 |差| 接近 360,不是真根;prev 为 0 的根已在上一点记过。 case prev != 0 && !math.IsNaN(prev) && !math.IsNaN(cur) && (prev < 0) != (cur < 0) && math.Abs(cur-prev) < 180: lo, hi, flo := prevT, t, prev // 迭代上限防止大 JDE 下浮点间隔大于容差时不收敛。 for i := 0; i < 100 && hi-lo > 1e-6; i++ { mid := (lo + hi) / 2 fm := f(mid) if fm == 0 { lo, hi = mid, mid break } if (fm < 0) == (flo < 0) { lo, flo = mid, fm } else { hi = mid } } roots = append(roots, (lo+hi)/2) } prevT, prev = t, cur } return roots } // SkyLongitudeCrossingsWithInstance body 视黄经经过 longitude 的全部时刻 / times when the apparent longitude equals longitude. func SkyLongitudeCrossingsWithInstance(body SkyBody, longitude, startJDE, endJDE float64, inst *Instance) []SkyEvent { if !body.valid() || !(endJDE > startJDE) { return nil } f := func(t float64) float64 { lo, _ := SkyBodyApparentLoBoWithInstance(body, t, inst) return geodata.NormalizeLongitude(lo - longitude) } var out []SkyEvent for _, t := range skyRoots(startJDE, endJDE, skyScanStep(body), f) { out = append(out, SkyEvent{JDE: t, Value: longitude}) } return out } // SkyStationsWithInstance 视黄经留(速度过零)的时刻,Value 为留时视黄经 / longitude stations; Value is the longitude. func SkyStationsWithInstance(body SkyBody, startJDE, endJDE float64, inst *Instance) []SkyEvent { if !body.valid() || body == SkySun || body == SkyMoon || !(endJDE > startJDE) { return nil } const h = 0.05 rate := func(t float64) float64 { a, _ := SkyBodyApparentLoBoWithInstance(body, t-h, inst) b, _ := SkyBodyApparentLoBoWithInstance(body, t+h, inst) return geodata.NormalizeLongitude(b - a) } var out []SkyEvent for _, t := range skyRoots(startJDE, endJDE, skyScanStep(body), rate) { lo, _ := SkyBodyApparentLoBoWithInstance(body, t, inst) out = append(out, SkyEvent{JDE: t, Value: lo}) } return out } // SkyAspectCrossingsWithInstance a 与 b 视黄经差(a − b)等于 angle 的时刻;angle 0 为黄经合 / times when lon(a) − lon(b) equals angle. func SkyAspectCrossingsWithInstance(a, b SkyBody, angle, startJDE, endJDE float64, inst *Instance) []SkyEvent { if !a.valid() || !b.valid() || a == b || !(endJDE > startJDE) { return nil } f := func(t float64) float64 { la, _ := SkyBodyApparentLoBoWithInstance(a, t, inst) lb, _ := SkyBodyApparentLoBoWithInstance(b, t, inst) return geodata.NormalizeLongitude(la - lb - angle) } var out []SkyEvent for _, t := range skyRoots(startJDE, endJDE, skyScanStep(a, b), f) { out = append(out, SkyEvent{JDE: t, Value: angle}) } return out } func skyAngularDistance(lon1, lat1, lon2, lat2 float64) float64 { v1, v2 := sphericalUnit(lon1, lat1), sphericalUnit(lon2, lat2) cross := [3]float64{v1[1]*v2[2] - v1[2]*v2[1], v1[2]*v2[0] - v1[0]*v2[2], v1[0]*v2[1] - v1[1]*v2[0]} return math.Atan2(math.Sqrt(cross[0]*cross[0]+cross[1]*cross[1]+cross[2]*cross[2]), v1[0]*v2[0]+v1[1]*v2[1]+v1[2]*v2[2]) / rad } // skyGoldenMin 黄金分割求 [lo, hi] 内单峰 f 的极小点。 func skyGoldenMin(lo, hi float64, f func(float64) float64) (float64, float64) { const g = 0.6180339887498949 x1, x2 := hi-g*(hi-lo), lo+g*(hi-lo) fx1, fx2 := f(x1), f(x2) for i := 0; i < 200 && hi-lo > 1e-6; i++ { if fx1 < fx2 { hi, x2, fx2 = x2, x1, fx1 x1 = hi - g*(hi-lo) fx1 = f(x1) } else { lo, x1, fx1 = x1, x2, fx2 x2 = lo + g*(hi-lo) fx2 = f(x2) } } t := (lo + hi) / 2 return t, f(t) } // skyMinima 扫描 f 在 [start, end] 内的局部极小并用黄金分割细化;只保留 ≤ maxValue 的极小。 func skyMinima(start, end, step, maxValue float64, f func(float64) float64) []SkyEvent { count, at, ok := skyGrid(start, end, step, 2) if !ok { return nil } var out []SkyEvent add := func(lo, hi, boundary float64) { // 端点段细化值须低于边界采样值,否则极小在窗口外,不是窗口内的局部极小。 if t, v := skyGoldenMin(lo, hi, f); v <= maxValue && v < boundary { out = append(out, SkyEvent{JDE: t, Value: v}) } } t0, t1 := at(0), at(1) f0, f1 := f(t0), f(t1) if f0 < f1 { add(t0, t1, f0) } for index := 2; index <= count; index++ { t2 := at(index) f2 := f(t2) if f1 <= f0 && f1 < f2 { add(t0, t2, math.Inf(1)) } t0, t1, f0, f1 = t1, t2, f1, f2 } if f1 < f0 { add(t0, t1, f1) } return out } // SkySeparationMinimaWithInstance a、b 地心视角距的局部极小(度),只返回 ≤ maxSeparation 的 / closest approaches within maxSeparation degrees. func SkySeparationMinimaWithInstance(a, b SkyBody, maxSeparation, startJDE, endJDE float64, inst *Instance) []SkyEvent { if !a.valid() || !b.valid() || a == b || !(endJDE > startJDE) { return nil } f := func(t float64) float64 { la, ba := SkyBodyApparentLoBoWithInstance(a, t, inst) lb, bb := SkyBodyApparentLoBoWithInstance(b, t, inst) return skyAngularDistance(la, ba, lb, bb) } return skyMinima(startJDE, endJDE, skyScanStep(a, b)/2, maxSeparation, f) } // SkyStarSeparationMinimaWithInstance 天体与恒星地心视角距的局部极小(度)/ closest approaches of a body to a star within maxSeparation degrees. func SkyStarSeparationMinimaWithInstance(body SkyBody, star StarCoordinate, maxSeparation, startJDE, endJDE float64, inst *Instance) []SkyEvent { if !body.valid() || !(endJDE > startJDE) { return nil } f := func(t float64) float64 { ra, dec := starApparentRaDecGeocentric(t, star, inst) sl, sb := RaDecToLoBo(t, ra, dec) lo, bo := SkyBodyApparentLoBoWithInstance(body, t, inst) return skyAngularDistance(lo, bo, sl, sb) } return skyMinima(startJDE, endJDE, skyScanStep(body)/2, maxSeparation, f) }