044f2f4381
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
168 lines
6.6 KiB
Go
168 lines
6.6 KiB
Go
// 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)
|
||
}
|