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,167 @@
|
||||
// Package refraction Saemundsson 大气折射及其数值逆解,供 basic 与 formula 共用。
|
||||
package refraction
|
||||
|
||||
import "math"
|
||||
|
||||
// StandardPressureHPa 与 StandardTemperatureC 是折射公式的标准气压与气温 / standard pressure and temperature of the refraction formula.
|
||||
const (
|
||||
StandardPressureHPa = 1010.0
|
||||
StandardTemperatureC = 10.0
|
||||
)
|
||||
|
||||
const (
|
||||
refractionStandardTemperatureK = 283.15
|
||||
refractionAbsoluteZeroC = -273.15
|
||||
refractionLowerLimitAltitudeDeg = -5.0
|
||||
refractionUpperLimitAltitudeDeg = 90.0
|
||||
)
|
||||
|
||||
// RefractionFromApparentAltitude 由视高度角求折射量(度,视高度减去它得真高度),无逆解时为 NaN / refraction from apparent altitude.
|
||||
func RefractionFromApparentAltitude(apparentAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
if !validRefractionInputs(apparentAltitude, pressureHPa, temperatureC) {
|
||||
return math.NaN()
|
||||
}
|
||||
if apparentAltitude <= refractionLowerLimitAltitudeDeg || apparentAltitude >= refractionUpperLimitAltitudeDeg {
|
||||
return 0
|
||||
}
|
||||
trueAltitude := trueAltitudeFromApparent(apparentAltitude, pressureHPa, temperatureC)
|
||||
return apparentAltitude - trueAltitude
|
||||
}
|
||||
|
||||
// TrueAltitude 视高度角转真高度角(度),无逆解时为 NaN / true altitude from apparent altitude.
|
||||
func TrueAltitude(apparentAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
if !validRefractionInputs(apparentAltitude, pressureHPa, temperatureC) {
|
||||
return math.NaN()
|
||||
}
|
||||
if apparentAltitude <= refractionLowerLimitAltitudeDeg || apparentAltitude >= refractionUpperLimitAltitudeDeg {
|
||||
return apparentAltitude
|
||||
}
|
||||
return trueAltitudeFromApparent(apparentAltitude, pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
// ApparentAltitude 真高度角转视高度角(度) / apparent altitude from true altitude.
|
||||
func ApparentAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
if !validRefractionInputs(trueAltitude, pressureHPa, temperatureC) {
|
||||
return math.NaN()
|
||||
}
|
||||
return trueAltitude + refractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
// RefractionFromTrueAltitude 由真高度角求折射量(度,加到真高度上得视高度) / refraction from true altitude.
|
||||
func RefractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
if !validRefractionInputs(trueAltitude, pressureHPa, temperatureC) {
|
||||
return math.NaN()
|
||||
}
|
||||
return refractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
// Saemundsson 公式以真高度角为输入;逆函数对同一模型数值求解,保证真/视高度互逆。
|
||||
func refractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
if trueAltitude <= refractionLowerLimitAltitudeDeg || trueAltitude >= refractionUpperLimitAltitudeDeg {
|
||||
return 0
|
||||
}
|
||||
angle := (trueAltitude + 10.3/(trueAltitude+5.11)) * math.Pi / 180
|
||||
return refractionScale(pressureHPa, temperatureC) * (1.02 / math.Tan(angle)) / 60
|
||||
}
|
||||
|
||||
func trueAltitudeFromApparent(apparentAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
lower := math.Nextafter(refractionLowerLimitAltitudeDeg, math.Inf(1))
|
||||
upper := apparentAltitude
|
||||
// 近 90° 时折射略为负,真高度高于视高度,根区间上界须放宽到 90°。
|
||||
if refractionFromTrueAltitude(apparentAltitude, pressureHPa, temperatureC) < 0 {
|
||||
upper = math.Nextafter(refractionUpperLimitAltitudeDeg, math.Inf(-1))
|
||||
}
|
||||
if estimate, ok := trueAltitudeFromApparentNewton(apparentAltitude, pressureHPa, temperatureC, lower, upper); ok {
|
||||
return estimate
|
||||
}
|
||||
return trueAltitudeFromApparentBisection(apparentAltitude, pressureHPa, temperatureC, lower, upper)
|
||||
}
|
||||
|
||||
func trueAltitudeFromApparentNewton(apparentAltitude, pressureHPa, temperatureC, lower, upper float64) (float64, bool) {
|
||||
estimate := apparentAltitude - refractionFromTrueAltitude(apparentAltitude, pressureHPa, temperatureC)
|
||||
scale := refractionScale(pressureHPa, temperatureC) * 1.02 / 60
|
||||
for i := 0; i < 12; i++ {
|
||||
if estimate < lower || estimate > upper || !finiteRefractionValue(estimate) {
|
||||
return 0, false
|
||||
}
|
||||
value := refractionInverseResidual(estimate, apparentAltitude, pressureHPa, temperatureC)
|
||||
if math.Abs(value) < 1e-12 {
|
||||
return estimate, true
|
||||
}
|
||||
shifted := estimate + 5.11
|
||||
sinAngle := math.Sin((estimate + 10.3/shifted) * math.Pi / 180)
|
||||
// d/dh cot(A) = −(dA/dh)/sin²A,A = h + 10.3/(h+5.11)(度)。
|
||||
derivative := 1 - scale*(1-10.3/(shifted*shifted))*(math.Pi/180)/(sinAngle*sinAngle)
|
||||
if math.Abs(derivative) < 1e-12 || !finiteRefractionValue(derivative) {
|
||||
return 0, false
|
||||
}
|
||||
next := estimate - value/derivative
|
||||
if next < lower || next > upper || !finiteRefractionValue(next) {
|
||||
return 0, false
|
||||
}
|
||||
estimate = next
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func trueAltitudeFromApparentBisection(apparentAltitude, pressureHPa, temperatureC, lower, upper float64) float64 {
|
||||
lowerValue := refractionInverseResidual(lower, apparentAltitude, pressureHPa, temperatureC)
|
||||
upperValue := refractionInverseResidual(upper, apparentAltitude, pressureHPa, temperatureC)
|
||||
if !finiteRefractionValue(lowerValue) || !finiteRefractionValue(upperValue) || lowerValue > 0 || upperValue < 0 {
|
||||
return math.NaN()
|
||||
}
|
||||
if math.Abs(lowerValue) < 1e-12 {
|
||||
return lower
|
||||
}
|
||||
if math.Abs(upperValue) < 1e-12 {
|
||||
return upper
|
||||
}
|
||||
|
||||
best, bestResidual := lower, math.Abs(lowerValue)
|
||||
if math.Abs(upperValue) < bestResidual {
|
||||
best, bestResidual = upper, math.Abs(upperValue)
|
||||
}
|
||||
for i := 0; i < 96; i++ {
|
||||
midpoint := lower + (upper-lower)/2
|
||||
if midpoint == lower || midpoint == upper {
|
||||
break
|
||||
}
|
||||
value := refractionInverseResidual(midpoint, apparentAltitude, pressureHPa, temperatureC)
|
||||
if !finiteRefractionValue(value) {
|
||||
return math.NaN()
|
||||
}
|
||||
if residual := math.Abs(value); residual < bestResidual {
|
||||
best, bestResidual = midpoint, residual
|
||||
}
|
||||
if math.Abs(value) < 1e-12 {
|
||||
return midpoint
|
||||
}
|
||||
if value > 0 {
|
||||
upper = midpoint
|
||||
} else {
|
||||
lower = midpoint
|
||||
}
|
||||
}
|
||||
if bestResidual <= 1e-9 {
|
||||
return best
|
||||
}
|
||||
return math.NaN()
|
||||
}
|
||||
|
||||
func refractionInverseResidual(trueAltitude, apparentAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
return trueAltitude + refractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC) - apparentAltitude
|
||||
}
|
||||
|
||||
func finiteRefractionValue(value float64) bool {
|
||||
return !math.IsNaN(value) && !math.IsInf(value, 0)
|
||||
}
|
||||
|
||||
func validRefractionInputs(altitude, pressureHPa, temperatureC float64) bool {
|
||||
return !(math.IsNaN(altitude) || math.IsInf(altitude, 0) ||
|
||||
math.IsNaN(pressureHPa) || math.IsInf(pressureHPa, 0) || pressureHPa <= 0 ||
|
||||
math.IsNaN(temperatureC) || math.IsInf(temperatureC, 0) || temperatureC <= refractionAbsoluteZeroC)
|
||||
}
|
||||
|
||||
func refractionScale(pressureHPa, temperatureC float64) float64 {
|
||||
return pressureHPa / StandardPressureHPa * refractionStandardTemperatureK / (temperatureC - refractionAbsoluteZeroC)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package refraction
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRefractionRoundTrip(t *testing.T) {
|
||||
for _, alt := range []float64{-1, 0, 0.5, 5, 30, 80} {
|
||||
apparent := ApparentAltitude(alt, StandardPressureHPa, StandardTemperatureC)
|
||||
if back := TrueAltitude(apparent, StandardPressureHPa, StandardTemperatureC); math.Abs(back-alt) > 1e-9 {
|
||||
t.Errorf("alt %v: round trip %v", alt, back)
|
||||
}
|
||||
}
|
||||
if r := RefractionFromTrueAltitude(0, 1010, 10) * 60; r < 28 || r > 36 {
|
||||
t.Errorf("horizon refraction %.2f arcmin", r)
|
||||
}
|
||||
if !math.IsNaN(RefractionFromTrueAltitude(0, 0, 10)) {
|
||||
t.Error("zero pressure must be NaN")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user