feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// occultationLimbTarget 给出某时刻目标的地心视赤经、赤纬(度)与地心距(km,恒星为 0)及站心角半径(角秒)。
|
||||
type occultationLimbTarget func(tt float64) (ra, dec, distanceKM, radiusArcsec float64, ok bool)
|
||||
|
||||
func planetOccultationLimbTarget(config planetOccultationConfig, observer Observer) occultationLimbTarget {
|
||||
return func(tt float64) (float64, float64, float64, float64, bool) {
|
||||
ra, dec := config.apparentRaDecN(tt, -1)
|
||||
distanceKM := config.earthDistanceN(tt, -1) * angularDiameterAstronomicalUnitKM
|
||||
radius := planetTopocentricSemidiameterN(tt, config, observer, -1)
|
||||
return ra, dec, distanceKM, radius, finite(ra) && finite(dec) && finite(distanceKM) && finite(radius)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
occultationLimbCrossingStepDays = 60.0 / 86400.0
|
||||
occultationLimbCrossingSpanDays = 3.5 / 24.0
|
||||
occultationLimbRootDays = 1e-3 / 86400.0
|
||||
occultationLimbMinSamples = 64
|
||||
occultationLimbMaxSamples = 4000
|
||||
occultationLimbBandPadArcsec = 0.05
|
||||
occultationLimbUnrangedPadKM = 1.0
|
||||
planetOccultationLimbStepDays = 0.25 / 86400.0
|
||||
)
|
||||
|
||||
// occultationLimbRangeKM 返回剖面半径范围;剖面不报告范围时在 tt 时刻朝目标扫一圈并外扩 1 km。
|
||||
func occultationLimbRangeKM(tt float64, limb LunarLimb, observer Observer, target occultationLimbTarget, inst *Instance) (float64, float64, bool) {
|
||||
geoRA, geoDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
|
||||
geoDistance := HMoonAwayNWithInstance(tt, -1, inst)
|
||||
frame := newLunarLimbFrame(limb, tt, lunarEclipseLimbVector(geoRA, geoDec, geoDistance), inst)
|
||||
if !frame.valid {
|
||||
return 0, 0, false
|
||||
}
|
||||
if frame.ranged {
|
||||
return frame.minKM, frame.maxKM, true
|
||||
}
|
||||
ra, dec, _, _, ok := target(tt)
|
||||
if !ok {
|
||||
return 0, 0, false
|
||||
}
|
||||
topoRA, topoDec := moonTopocentricApparentRaDec(tt, observer, -1, inst)
|
||||
view, ok := frame.viewToward(lunarEclipseLimbVector(topoRA, topoDec, 1), lunarEclipseLimbVector(ra, dec, 1))
|
||||
if !ok {
|
||||
return 0, 0, false
|
||||
}
|
||||
low, high := math.Inf(1), math.Inf(-1)
|
||||
for offset := 0.0; offset < 360; offset += frame.stepDeg {
|
||||
radius, ok := frame.radiusAt(view, offset)
|
||||
if !ok {
|
||||
return 0, 0, false
|
||||
}
|
||||
low, high = math.Min(low, radius), math.Max(high, radius)
|
||||
}
|
||||
return low - occultationLimbUnrangedPadKM, high + occultationLimbUnrangedPadKM, true
|
||||
}
|
||||
|
||||
// occultationLimbCrossing 自 greatestTT 向 direction 一侧求圆月面间隙升到 level 的时刻;间隙自掩甚单调增大,起点已不低于 level 时返回起点。
|
||||
func occultationLimbCrossing(greatestTT, direction, level float64, circular func(float64) (float64, bool)) (float64, bool) {
|
||||
near := greatestTT
|
||||
value, ok := circular(near)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
if value >= level {
|
||||
return greatestTT, true
|
||||
}
|
||||
for step := 1; float64(step)*occultationLimbCrossingStepDays <= occultationLimbCrossingSpanDays; step++ {
|
||||
far := greatestTT + direction*float64(step)*occultationLimbCrossingStepDays
|
||||
farValue, ok := circular(far)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
if farValue < level {
|
||||
near = far
|
||||
continue
|
||||
}
|
||||
for i := 0; i < 64 && math.Abs(far-near) > occultationLimbRootDays; i++ {
|
||||
mid := (near + far) / 2
|
||||
midValue, ok := circular(mid)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
if midValue < level {
|
||||
near = mid
|
||||
} else {
|
||||
far = mid
|
||||
}
|
||||
}
|
||||
return (near + far) / 2, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// occultationLimbOuterRoots 在 [start, end] 上采样月缘间隙,返回首个由正转负与最后一个由负转正的零点;
|
||||
// 端点状态由调用方保证(hiddenAtStart、hiddenAtEnd),ok 为假表示剖面无值或星历失败。
|
||||
func occultationLimbOuterRoots(start, end float64, hiddenAtStart, hiddenAtEnd bool, gap func(float64) (float64, bool)) (first, last float64, found, ok bool) {
|
||||
if end <= start {
|
||||
return 0, 0, false, true
|
||||
}
|
||||
count := int(math.Ceil((end - start) / planetOccultationLimbStepDays))
|
||||
if count < occultationLimbMinSamples {
|
||||
count = occultationLimbMinSamples
|
||||
}
|
||||
if count > occultationLimbMaxSamples {
|
||||
count = occultationLimbMaxSamples
|
||||
}
|
||||
first, last = math.NaN(), math.NaN()
|
||||
previousTT, previousHidden := start, hiddenAtStart
|
||||
for index := 0; index <= count; index++ {
|
||||
tt := start + (end-start)*float64(index)/float64(count)
|
||||
value, valueOK := gap(tt)
|
||||
if !valueOK {
|
||||
return 0, 0, false, false
|
||||
}
|
||||
hidden := value < 0
|
||||
if index == 0 {
|
||||
hidden = hiddenAtStart
|
||||
} else if index == count {
|
||||
hidden = hiddenAtEnd
|
||||
}
|
||||
if index > 0 && hidden != previousHidden {
|
||||
root := occultationLimbBisect(previousTT, tt, previousHidden, gap)
|
||||
if hidden && math.IsNaN(first) {
|
||||
first = root
|
||||
}
|
||||
if !hidden {
|
||||
last = root
|
||||
}
|
||||
}
|
||||
previousTT, previousHidden = tt, hidden
|
||||
}
|
||||
return first, last, !math.IsNaN(first) || !math.IsNaN(last), true
|
||||
}
|
||||
|
||||
// occultationLimbBisect 在相邻样本间求遮挡状态翻转的时刻;leftHidden 是左端的遮挡状态。
|
||||
func occultationLimbBisect(left, right float64, leftHidden bool, gap func(float64) (float64, bool)) float64 {
|
||||
for i := 0; i < 64 && right-left > occultationLimbRootDays; i++ {
|
||||
mid := (left + right) / 2
|
||||
value, ok := gap(mid)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if (value < 0) == leftHidden {
|
||||
left = mid
|
||||
} else {
|
||||
right = mid
|
||||
}
|
||||
}
|
||||
return (left + right) / 2
|
||||
}
|
||||
|
||||
// planetOccultationInfoWithLimb 有月缘时按剖面判定行星月掩:只在圆月面间隙落进月缘起伏带的时段采样全方向月缘间隙,
|
||||
// 取最外侧的外切与内切;月缘可新增或取消圆月面的掠掩与全掩,有正持续时间的接触一律按偏掩或全掩上报。剖面无值时退回圆月面结果。
|
||||
func planetOccultationInfoWithLimb(greatestTT float64, config planetOccultationConfig, observer Observer, safetyMarginArcsec float64, location *time.Location, limb LunarLimb, inst *Instance) (PlanetOccultationInfo, bool) {
|
||||
circularInfo, circularOK := planetOccultationInfoAtGreatest(greatestTT, config, observer, safetyMarginArcsec, location, inst)
|
||||
if limb == nil {
|
||||
return circularInfo, circularOK
|
||||
}
|
||||
info, state, ok := planetOccultationGreatestInfo(greatestTT, config, observer, safetyMarginArcsec, location, inst)
|
||||
if !ok {
|
||||
return circularInfo, circularOK
|
||||
}
|
||||
target := planetOccultationLimbTarget(config, observer)
|
||||
lowKM, highKM, ok := occultationLimbRangeKM(greatestTT, limb, observer, target, inst)
|
||||
if !ok {
|
||||
return circularInfo, circularOK
|
||||
}
|
||||
ephemeris, ok := newPlanetLimbEphemeris(greatestTT, config, limb, inst)
|
||||
if !ok {
|
||||
return circularInfo, circularOK
|
||||
}
|
||||
// 月缘间隙 ≈ 圆月面间隙 −(月缘半径 − 参考半径),所以圆月面间隙不低于 bandHigh 必不接触、不高于 bandLow 必已接触。
|
||||
// 起伏带以角秒计,外扩一点容差覆盖月心距在事件内的变化。
|
||||
scale := state.moonSemidiameter / moonEquatorialRadiusKM
|
||||
bandLow := (lowKM-moonEquatorialRadiusKM)*scale - occultationLimbBandPadArcsec
|
||||
bandHigh := (highKM-moonEquatorialRadiusKM)*scale + occultationLimbBandPadArcsec
|
||||
circular := func(internal bool) func(float64) (float64, bool) {
|
||||
return func(tt float64) (float64, bool) { return ephemeris.circularGap(tt, observer, internal) }
|
||||
}
|
||||
limbGap := func(internal bool) func(float64) (float64, bool) {
|
||||
return func(tt float64) (float64, bool) { return ephemeris.limbGap(tt, observer, internal) }
|
||||
}
|
||||
// solve 求一种接触的最外侧掩入、复现:圆月面间隙在 [bandLow, bandHigh] 之外的两端遮挡状态已知,只在带内采样。
|
||||
solve := func(internal bool, circularMinimum float64) (float64, float64, bool, bool) {
|
||||
if circularMinimum >= bandHigh {
|
||||
return 0, 0, false, true
|
||||
}
|
||||
outerLeft, okLeft := occultationLimbCrossing(greatestTT, -1, bandHigh, circular(internal))
|
||||
outerRight, okRight := occultationLimbCrossing(greatestTT, 1, bandHigh, circular(internal))
|
||||
if !okLeft || !okRight {
|
||||
return 0, 0, false, false
|
||||
}
|
||||
if circularMinimum > bandLow {
|
||||
first, last, found, ok := occultationLimbOuterRoots(outerLeft, outerRight, false, false, limbGap(internal))
|
||||
return first, last, found && !math.IsNaN(first) && !math.IsNaN(last), ok
|
||||
}
|
||||
innerLeft, okLeft := occultationLimbCrossing(greatestTT, -1, bandLow, circular(internal))
|
||||
innerRight, okRight := occultationLimbCrossing(greatestTT, 1, bandLow, circular(internal))
|
||||
if !okLeft || !okRight {
|
||||
return 0, 0, false, false
|
||||
}
|
||||
first, _, foundLeft, okLeftRoots := occultationLimbOuterRoots(outerLeft, innerLeft, false, true, limbGap(internal))
|
||||
_, last, foundRight, okRightRoots := occultationLimbOuterRoots(innerRight, outerRight, true, false, limbGap(internal))
|
||||
return first, last, foundLeft && foundRight && !math.IsNaN(first) && !math.IsNaN(last), okLeftRoots && okRightRoots
|
||||
}
|
||||
externalImmersion, externalEmersion, external, ok := solve(false, state.externalContactMetric)
|
||||
if !ok {
|
||||
return circularInfo, circularOK
|
||||
}
|
||||
if !external {
|
||||
return PlanetOccultationInfo{}, false
|
||||
}
|
||||
internalImmersion, internalEmersion, internal, ok := solve(true, state.internalContactMetric)
|
||||
if !ok {
|
||||
return circularInfo, circularOK
|
||||
}
|
||||
info.Type = OccultationPartial
|
||||
info.ExternalImmersion = occultationTTToLocation(externalImmersion, location)
|
||||
info.ExternalEmersion = occultationTTToLocation(externalEmersion, location)
|
||||
if internal && internalImmersion > externalImmersion && internalEmersion < externalEmersion && internalImmersion < internalEmersion {
|
||||
info.Type = OccultationTotal
|
||||
info.HasInternalContacts = true
|
||||
info.InternalImmersion = occultationTTToLocation(internalImmersion, location)
|
||||
info.InternalEmersion = occultationTTToLocation(internalEmersion, location)
|
||||
}
|
||||
info.ContactsComplete = true
|
||||
info.LunarLimbApplied = true
|
||||
return info, true
|
||||
}
|
||||
|
||||
// planetLimbEphemeris 掩甚前后 ±4 h 的地心月球、行星视位置与月球本体坐标架插值表;站心换算逐点精确计算。
|
||||
type planetLimbEphemeris struct {
|
||||
bodies []localEphemerisVectorNode
|
||||
axes []localEphemerisVectorNode
|
||||
base lunarLimbFrame
|
||||
radiusKM float64
|
||||
}
|
||||
|
||||
func newPlanetLimbEphemeris(greatestTT float64, config planetOccultationConfig, limb LunarLimb, inst *Instance) (*planetLimbEphemeris, bool) {
|
||||
half := int(math.Round(starOccultationLimbNodeSpan / starOccultationLimbNodeDays))
|
||||
ephemeris := &planetLimbEphemeris{
|
||||
bodies: make([]localEphemerisVectorNode, 2*half+1),
|
||||
axes: make([]localEphemerisVectorNode, 2*half+1),
|
||||
radiusKM: config.equatorialRadiusKM,
|
||||
}
|
||||
for index := range ephemeris.bodies {
|
||||
tt := greatestTT + float64(index-half)*starOccultationLimbNodeDays
|
||||
moonRA, moonDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
|
||||
moon := lunarEclipseLimbVector(moonRA, moonDec, HMoonAwayNWithInstance(tt, -1, inst))
|
||||
planetRA, planetDec := config.apparentRaDecN(tt, -1)
|
||||
planet := lunarEclipseLimbVector(planetRA, planetDec, config.earthDistanceN(tt, -1)*angularDiameterAstronomicalUnitKM)
|
||||
frame := newLunarLimbFrame(limb, tt, moon, inst)
|
||||
if !frame.valid || !finiteVector3(moon) || !finiteVector3(planet) {
|
||||
return nil, false
|
||||
}
|
||||
if index == half {
|
||||
ephemeris.base = frame
|
||||
}
|
||||
ephemeris.bodies[index] = localEphemerisVectorNode{tt: tt, first: moon, next: planet}
|
||||
ephemeris.axes[index] = localEphemerisVectorNode{tt: tt, first: frame.axisX, next: frame.axisZ}
|
||||
}
|
||||
return ephemeris, true
|
||||
}
|
||||
|
||||
// planetLimbTopocentric 是插值得到的站心月球、行星方向与距离(km),以及该时刻的月球本体坐标架。
|
||||
type planetLimbTopocentric struct {
|
||||
moon, planet [3]float64
|
||||
moonDistance, planetRadius float64
|
||||
frame lunarLimbFrame
|
||||
}
|
||||
|
||||
func (ephemeris *planetLimbEphemeris) topocentricAt(tt float64, observer Observer) (planetLimbTopocentric, bool) {
|
||||
moon, planet, ok := interpolateLocalEphemerisVectors(ephemeris.bodies, tt)
|
||||
if !ok {
|
||||
return planetLimbTopocentric{}, false
|
||||
}
|
||||
axisX, axisZ, ok := interpolateLocalEphemerisVectors(ephemeris.axes, tt)
|
||||
if !ok {
|
||||
return planetLimbTopocentric{}, false
|
||||
}
|
||||
ut := TT2UTC(tt)
|
||||
toTopocentric := func(vector [3]float64) (float64, float64, float64, bool) {
|
||||
distance := math.Sqrt(lunarLimbDot(vector, vector))
|
||||
ra := normalizeRA(math.Atan2(vector[1], vector[0]) / rad)
|
||||
dec := math.Asin(clampUnit(vector[2]/distance)) / rad
|
||||
topoRA, topoDec := TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, ut, distance/angularDiameterAstronomicalUnitKM, observer.Height)
|
||||
topoDistance := topocentricDistanceKM(ra, dec, distance, observer, ut)
|
||||
return normalizeRA(topoRA), topoDec, topoDistance, finite(topoRA) && finite(topoDec) && finite(topoDistance) && topoDistance > 0
|
||||
}
|
||||
moonRA, moonDec, moonDistance, okMoon := toTopocentric(moon)
|
||||
planetRA, planetDec, planetDistance, okPlanet := toTopocentric(planet)
|
||||
if !okMoon || !okPlanet || planetDistance <= ephemeris.radiusKM {
|
||||
return planetLimbTopocentric{}, false
|
||||
}
|
||||
frame := ephemeris.base
|
||||
if frame.axisZ, ok = lunarLimbNormalize(axisZ); !ok {
|
||||
return planetLimbTopocentric{}, false
|
||||
}
|
||||
if frame.axisX, ok = lunarLimbNormalize(lunarLimbReject(axisX, frame.axisZ)); !ok {
|
||||
return planetLimbTopocentric{}, false
|
||||
}
|
||||
frame.axisY = lunarLimbCross(frame.axisZ, frame.axisX)
|
||||
return planetLimbTopocentric{
|
||||
moon: lunarEclipseLimbVector(moonRA, moonDec, moonDistance),
|
||||
planet: lunarEclipseLimbVector(planetRA, planetDec, 1),
|
||||
moonDistance: moonDistance,
|
||||
planetRadius: math.Asin(ephemeris.radiusKM / planetDistance),
|
||||
frame: frame,
|
||||
}, true
|
||||
}
|
||||
|
||||
// circularGap 圆月面外切或内切间隙(角秒),与 planetOccultationStateAt 同口径。
|
||||
func (ephemeris *planetLimbEphemeris) circularGap(tt float64, observer Observer, internal bool) (float64, bool) {
|
||||
state, ok := ephemeris.topocentricAt(tt, observer)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
separation := math.Acos(clampUnit(lunarLimbDot(state.moon, state.planet) / state.moonDistance))
|
||||
moonRadius := math.Asin(moonEquatorialRadiusKM / state.moonDistance)
|
||||
if internal {
|
||||
return (separation - moonRadius + state.planetRadius) * angularDiameterArcsecPerRadian, true
|
||||
}
|
||||
return (separation - moonRadius - state.planetRadius) * angularDiameterArcsecPerRadian, true
|
||||
}
|
||||
|
||||
// limbGap 全方向月缘外切或内切间隙(角秒)。
|
||||
func (ephemeris *planetLimbEphemeris) limbGap(tt float64, observer Observer, internal bool) (float64, bool) {
|
||||
state, ok := ephemeris.topocentricAt(tt, observer)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
view, ok := state.frame.viewToward(state.moon, state.planet)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
contact := lunarLimbContact{
|
||||
frame: &state.frame,
|
||||
view: view,
|
||||
distanceKM: state.moonDistance,
|
||||
separation: math.Acos(clampUnit(lunarLimbDot(state.moon, state.planet) / state.moonDistance)),
|
||||
targetRadius: state.planetRadius,
|
||||
}
|
||||
if internal {
|
||||
inner, ok := contact.innerRadius(true)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return (contact.separation - math.Abs(inner-contact.targetRadius)) * angularDiameterArcsecPerRadian, true
|
||||
}
|
||||
outer, ok := contact.outerRadius()
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return (contact.separation - outer - contact.targetRadius) * angularDiameterArcsecPerRadian, true
|
||||
}
|
||||
Reference in New Issue
Block a user