270 lines
8.7 KiB
Go
270 lines
8.7 KiB
Go
|
|
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)
|
|||
|
|
}
|