044f2f4381
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
273 lines
11 KiB
Go
273 lines
11 KiB
Go
package basic
|
||
|
||
import (
|
||
"math"
|
||
"sort"
|
||
)
|
||
|
||
const (
|
||
schaeferNightSkyNanolambert = 180.0
|
||
schaeferNanolambertToErg = 1.02e-15
|
||
schaeferSunMagnitude = -26.74
|
||
schaeferZeroMagnitudeRef = -11.05
|
||
schaeferMinSeparationDeg = 0.1
|
||
schaeferTwilightLowSunDeg = -3.0
|
||
schaeferDaylightHighSunDeg = 4.0
|
||
schaeferScotopicLimitNL = 1479.1
|
||
)
|
||
|
||
// HeliacalOptions 偕日可见计算参数;零值使用消光系数 0.25 星等/大气质量、目标最低高度 0.5° / heliacal visibility options.
|
||
type HeliacalOptions struct {
|
||
// ExtinctionCoefficient 大气消光系数(星等/大气质量)/ extinction in magnitudes per airmass.
|
||
ExtinctionCoefficient float64
|
||
// MinTargetAltitude 目标最低几何高度(度)/ minimum geometric target altitude in degrees.
|
||
MinTargetAltitude float64
|
||
// Moonlight 为 true 时计入月光散射(Krisciunas–Schaefer);默认不计 / include scattered moonlight when true.
|
||
Moonlight bool
|
||
}
|
||
|
||
func (options HeliacalOptions) normalized() HeliacalOptions {
|
||
if !(options.ExtinctionCoefficient > 0) {
|
||
options.ExtinctionCoefficient = 0.25
|
||
}
|
||
if !(options.MinTargetAltitude > 0) {
|
||
options.MinTargetAltitude = 0.5
|
||
}
|
||
return options
|
||
}
|
||
|
||
func schaeferAirmass(altitudeDeg float64) float64 {
|
||
z := math.Min(90, math.Max(0, 90-altitudeDeg)) * rad
|
||
c := math.Cos(z)
|
||
return 1 / (c + 0.025*math.Exp(-11*c))
|
||
}
|
||
|
||
func schaeferTransmission(k, airmass float64) float64 { return math.Pow(10, -0.4*k*airmass) }
|
||
|
||
func schaeferPhase(separationDeg float64) float64 {
|
||
s := math.Max(schaeferMinSeparationDeg, separationDeg)
|
||
c := math.Cos(s * rad)
|
||
return 6.2e7/(s*s) + math.Pow(10, 6.15-s/40) + math.Pow(10, 5.36)*(1.06+c*c)
|
||
}
|
||
|
||
func validExtinction(extinction float64) bool {
|
||
return extinction > 0 && !math.IsInf(extinction, 0)
|
||
}
|
||
|
||
// SkyBrightnessNanolambert 无月夜晨昏天光亮度(nL,Schaefer 模型),消光系数须为正,否则返回 NaN / twilight sky brightness without moonlight, Schaefer model.
|
||
func SkyBrightnessNanolambert(targetAltitude, sunAltitude, separation, extinction float64) float64 {
|
||
if !validExtinction(extinction) {
|
||
return math.NaN()
|
||
}
|
||
k := extinction
|
||
targetT := schaeferTransmission(k, schaeferAirmass(targetAltitude))
|
||
sunT := schaeferTransmission(k, schaeferAirmass(sunAltitude))
|
||
targetZ := math.Min(90, math.Max(0, 90-targetAltitude))
|
||
sunZ := math.Min(180, math.Max(0, 90-sunAltitude))
|
||
sep := math.Max(schaeferMinSeparationDeg, separation)
|
||
scale := math.Pow(10, -0.4*(schaeferSunMagnitude-schaeferZeroMagnitudeRef+43.27))
|
||
day := scale * (1 - targetT) * (schaeferPhase(sep)*sunT + 440000*(1-sunT))
|
||
twilight := math.Pow(10, -0.4*(schaeferSunMagnitude-schaeferZeroMagnitudeRef-57.5+sunZ-targetZ/(360*k))) *
|
||
(1 - targetT) * 100 / sep
|
||
sky := math.Min(day, twilight)
|
||
switch {
|
||
case sunAltitude < schaeferTwilightLowSunDeg:
|
||
sky = twilight
|
||
case sunAltitude > schaeferDaylightHighSunDeg:
|
||
sky = day
|
||
}
|
||
sz := math.Sin(targetZ * rad)
|
||
night := schaeferNightSkyNanolambert * (0.4 + 0.6/math.Sqrt(1-0.96*sz*sz)) * targetT
|
||
return sky/schaeferNanolambertToErg + night
|
||
}
|
||
|
||
// LimitingMagnitude 肉眼极限星等(已扣除目标方向的大气消光),消光系数须为正,否则返回 NaN / naked-eye limiting magnitude after extinction at the target altitude.
|
||
func LimitingMagnitude(targetAltitude, sunAltitude, separation, extinction float64) float64 {
|
||
return limitingMagnitudeForSky(SkyBrightnessNanolambert(targetAltitude, sunAltitude, separation, extinction), targetAltitude, extinction)
|
||
}
|
||
|
||
// MoonlightNanolambert 月光在目标方向造成的天光亮度(nL,Krisciunas–Schaefer);月在地平下为 0,消光系数非正为 NaN /
|
||
// sky brightness from scattered moonlight; zero when the Moon is below the horizon.
|
||
func MoonlightNanolambert(targetAltitude, moonAltitude, moonSeparation, moonPhaseAngle, extinction float64) float64 {
|
||
if !validExtinction(extinction) {
|
||
return math.NaN()
|
||
}
|
||
if moonAltitude <= 0 || targetAltitude <= 0 {
|
||
return 0
|
||
}
|
||
alpha := math.Abs(moonPhaseAngle)
|
||
mag := -12.73 + 0.026*alpha + 4e-9*math.Pow(alpha, 4)
|
||
illuminance := math.Pow(10, -0.4*(mag+16.57))
|
||
rho := math.Max(schaeferMinSeparationDeg, moonSeparation)
|
||
c := math.Cos(rho * rad)
|
||
scatter := math.Pow(10, 5.36)*(1.06+c*c) + math.Pow(10, 6.15-rho/40)
|
||
airmass := func(alt float64) float64 {
|
||
s := math.Sin((90 - alt) * rad)
|
||
return 1 / math.Sqrt(1-0.96*s*s)
|
||
}
|
||
return scatter * illuminance * math.Pow(10, -0.4*extinction*airmass(moonAltitude)) *
|
||
(1 - math.Pow(10, -0.4*extinction*airmass(targetAltitude)))
|
||
}
|
||
|
||
func limitingMagnitudeForSky(b, targetAltitude, extinction float64) float64 {
|
||
c, k := 4.4668359215096e-9, 1.2589254117942e-6
|
||
if b < schaeferScotopicLimitNL {
|
||
c, k = 1.5848931924611e-10, 0.012589254117942
|
||
}
|
||
threshold := c * math.Pow(1+math.Sqrt(k*b), 2)
|
||
return -16.57 - 2.5*math.Log10(threshold) - extinction*schaeferAirmass(targetAltitude)
|
||
}
|
||
|
||
// HeliacalEventKind 偕日事件类型 / heliacal event kind.
|
||
type HeliacalEventKind int
|
||
|
||
const (
|
||
// HeliacalMorningFirst 晨始见(偕日升)/ morning first visibility.
|
||
HeliacalMorningFirst HeliacalEventKind = iota
|
||
// HeliacalMorningLast 晨末见 / morning last visibility.
|
||
HeliacalMorningLast
|
||
// HeliacalEveningFirst 昏始见 / evening first visibility.
|
||
HeliacalEveningFirst
|
||
// HeliacalEveningLast 昏末见(偕日落)/ evening last visibility.
|
||
HeliacalEveningLast
|
||
)
|
||
|
||
// HeliacalEvent 偕日事件:JD 为当天最佳可见时刻(UT 儒略日),Margin 为极限星等减目标星等 / heliacal event at the best moment of that twilight.
|
||
type HeliacalEvent struct {
|
||
Kind HeliacalEventKind
|
||
JD float64
|
||
Margin float64
|
||
TargetAltitude float64
|
||
SunAltitude float64
|
||
}
|
||
|
||
// heliacalTarget 返回 TT 时刻目标的视赤经、赤纬(度)与视星等。
|
||
type heliacalTarget func(jde float64) (ra, dec, magnitude float64)
|
||
|
||
type heliacalBest struct {
|
||
jd, margin, targetAlt, sunAlt float64
|
||
visible bool
|
||
}
|
||
|
||
func heliacalAltitude(ra, dec, lst, lat float64) float64 {
|
||
h := (lst - ra) * rad
|
||
d, p := dec*rad, lat*rad
|
||
return math.Asin(math.Sin(p)*math.Sin(d)+math.Cos(p)*math.Cos(d)*math.Cos(h)) / rad
|
||
}
|
||
|
||
// heliacalTwilight 扫描一次晨(morning)或昏的太阳高度 −18°..0° 时段,取可见余量最大的样本。
|
||
// 日月位置在窗口中点取一次,窗口不足两小时,引入的高度误差远小于模型本身。
|
||
func heliacalTwilight(dayJD, lon, lat float64, morning bool, target heliacalTarget, options HeliacalOptions, inst *Instance) heliacalBest {
|
||
center := dayJD - lon/360 + 0.5
|
||
if morning {
|
||
center -= 0.25
|
||
} else {
|
||
center += 0.25
|
||
}
|
||
jde := UTC2TT(center)
|
||
sunRA, sunDec := HSunApparentRaDecWithInstance(jde, inst)
|
||
ra, dec, mag := target(jde)
|
||
sep := skyAngularDistance(ra, dec, sunRA, sunDec)
|
||
best := heliacalBest{margin: math.Inf(-1)}
|
||
if math.IsNaN(ra) || math.IsNaN(mag) {
|
||
return best
|
||
}
|
||
var moonRA, moonDec, moonSep, moonPhase float64
|
||
if options.Moonlight {
|
||
lo, bo := SkyBodyApparentLoBoWithInstance(SkyMoon, jde, inst)
|
||
moonRA, moonDec = LoBoToRaDec(jde, lo, bo)
|
||
moonSep = skyAngularDistance(ra, dec, moonRA, moonDec)
|
||
moonPhase = 180 - skyAngularDistance(moonRA, moonDec, sunRA, sunDec)
|
||
}
|
||
lst0 := ApparentSiderealTime(UTC2UT1(center))*15 + lon
|
||
const step = 2.0 / 1440
|
||
for dt := -0.25; dt <= 0.25; dt += step {
|
||
lst := lst0 + dt*360.98564736629
|
||
sunAlt := heliacalAltitude(sunRA, sunDec, lst, lat)
|
||
if sunAlt > 0 || sunAlt < -18 {
|
||
continue
|
||
}
|
||
alt := heliacalAltitude(ra, dec, lst, lat)
|
||
if alt < options.MinTargetAltitude {
|
||
continue
|
||
}
|
||
sky := SkyBrightnessNanolambert(alt, sunAlt, sep, options.ExtinctionCoefficient)
|
||
if options.Moonlight {
|
||
sky += MoonlightNanolambert(alt, heliacalAltitude(moonRA, moonDec, lst, lat), moonSep, moonPhase, options.ExtinctionCoefficient)
|
||
}
|
||
margin := limitingMagnitudeForSky(sky, alt, options.ExtinctionCoefficient) - mag
|
||
if margin > best.margin {
|
||
best = heliacalBest{center + dt, margin, alt, sunAlt, margin >= 0}
|
||
}
|
||
}
|
||
return best
|
||
}
|
||
|
||
// heliacalMaxDays 逐日扫描的天数上限(约 2.3 万年),挡住非法跨度造成的近乎无界扫描。
|
||
const heliacalMaxDays = 1 << 23
|
||
|
||
// findHeliacalEvents 逐日扫描晨、昏可见性,从不可见变为可见记“始见”,反之记“末见”(取末个可见日)。
|
||
func findHeliacalEvents(startJD, endJD, lon, lat float64, target heliacalTarget, options HeliacalOptions, inst *Instance) []HeliacalEvent {
|
||
options = options.normalized()
|
||
if !finite(startJD) || !finite(endJD) || !(endJD > startJD) || math.Abs(lat) >= 90 {
|
||
return nil
|
||
}
|
||
day0 := math.Floor(startJD-0.5) + 0.5
|
||
span := math.Floor(endJD - day0)
|
||
if !(span < heliacalMaxDays) {
|
||
return nil
|
||
}
|
||
days := int(span)
|
||
var out []HeliacalEvent
|
||
for _, morning := range []bool{true, false} {
|
||
first, last := HeliacalEveningFirst, HeliacalEveningLast
|
||
if morning {
|
||
first, last = HeliacalMorningFirst, HeliacalMorningLast
|
||
}
|
||
prev := heliacalTwilight(day0-1, lon, lat, morning, target, options, inst)
|
||
for i := 0; i <= days; i++ {
|
||
cur := heliacalTwilight(day0+float64(i), lon, lat, morning, target, options, inst)
|
||
// 事件时刻是采样日内的细化值,可能越过 endJD,所以上界按事件时刻判断,不能按采样日判断。
|
||
switch {
|
||
case cur.visible && !prev.visible && cur.jd >= startJD && cur.jd <= endJD:
|
||
out = append(out, HeliacalEvent{first, cur.jd, cur.margin, cur.targetAlt, cur.sunAlt})
|
||
case !cur.visible && prev.visible && prev.jd >= startJD && prev.jd <= endJD:
|
||
out = append(out, HeliacalEvent{last, prev.jd, prev.margin, prev.targetAlt, prev.sunAlt})
|
||
}
|
||
prev = cur
|
||
}
|
||
}
|
||
sortHeliacalEvents(out)
|
||
return out
|
||
}
|
||
|
||
func sortHeliacalEvents(events []HeliacalEvent) {
|
||
sort.SliceStable(events, func(i, j int) bool { return events[i].JD < events[j].JD })
|
||
}
|
||
|
||
// FindPlanetHeliacalEventsWithInstance 行星偕日事件(UT 儒略日范围,经度东正)/ planet heliacal events in a UT JD range.
|
||
func FindPlanetHeliacalEventsWithInstance(body SkyBody, startJD, endJD, lon, lat float64, options HeliacalOptions, inst *Instance) []HeliacalEvent {
|
||
if body < SkyMercury || body > SkyNeptune {
|
||
return nil
|
||
}
|
||
index := int(body-SkyMercury) + 1
|
||
target := func(jde float64) (float64, float64, float64) {
|
||
lo, bo := SkyBodyApparentLoBoWithInstance(body, jde, inst)
|
||
ra, dec := LoBoToRaDec(jde, lo, bo)
|
||
return ra, dec, planetMagnitude2018(index, jde, inst)
|
||
}
|
||
return findHeliacalEvents(startJD, endJD, lon, lat, target, options, inst)
|
||
}
|
||
|
||
// FindStarHeliacalEventsWithInstance 恒星偕日事件;magnitude 为恒星 V 星等 / star heliacal events with the star's V magnitude.
|
||
func FindStarHeliacalEventsWithInstance(star StarCoordinate, magnitude, startJD, endJD, lon, lat float64, options HeliacalOptions, inst *Instance) []HeliacalEvent {
|
||
if math.IsNaN(magnitude) || math.IsInf(magnitude, 0) {
|
||
return nil
|
||
}
|
||
target := func(jde float64) (float64, float64, float64) {
|
||
ra, dec := starApparentRaDecGeocentric(jde, star, inst)
|
||
return ra, dec, magnitude
|
||
}
|
||
return findHeliacalEvents(startJD, endJD, lon, lat, target, options, inst)
|
||
}
|