astro/orbit/visual_binary.go
starainrt 3ffdbe0034
feat: 扩展天文计算能力
- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息
- 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算
- 新增木星伽利略卫星位置、现象与接触事件计算
- 新增恒星星表、星座判定、自行修正与观测辅助能力
- 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包
- 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算
- 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试
- 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试
- 更新中文和英文 README,补充示例、精度说明、SVG 配图
2026-05-01 22:38:44 +08:00

159 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package orbit
import (
"math"
"time"
)
const visualBinaryDeg = 180 / math.Pi
const visualBinaryRad = math.Pi / 180
// VisualBinaryElements 视双星轨道要素,采用《天文算法》第 55 章的经典口径。
type VisualBinaryElements struct {
PeriodYears float64 // 周期 P单位平太阳年 / orbital period in mean solar years.
PeriastronYear float64 // 过近星点时刻 T采用带小数的年 / epoch of periastron as a decimal year.
Eccentricity float64 // 离心率 e / eccentricity.
SemiMajorAxis float64 // 半长轴 a单位角秒 / semi-major axis in arcseconds.
Inclination float64 // 倾角 i单位度 / inclination in degrees.
AscendingNode float64 // 升交点位置角 Ω,单位度 / position angle of ascending node in degrees.
PeriastronArgument float64 // 近星点角距 ω,单位度 / argument of periastron in degrees.
}
// VisualBinaryPosition 视双星在天球上的计算结果。
type VisualBinaryPosition struct {
Year float64 // 计算使用的小数年 / decimal year used for the evaluation.
MeanAnomaly float64 // 平近点角 M单位度 / mean anomaly in degrees.
EccentricAnomaly float64 // 偏近点角 E单位度 / eccentric anomaly in degrees.
TrueAnomaly float64 // 真近点角 v单位度 / true anomaly in degrees.
Radius float64 // 径矢 r单位角秒 / radius vector in arcseconds.
PositionAngle float64 // 位置角 θ,北为 0°、东为 90° / position angle, north through east.
Separation float64 // 角距离 ρ,单位角秒 / apparent separation in arcseconds.
}
// VisualBinary 视双星位置 / visual binary position.
//
// 输入时刻会先换算为 UTC 小数年,再按经典视轨道公式求解。
// The input instant is converted to a UTC decimal year before evaluation.
func VisualBinary(date time.Time, elements VisualBinaryElements) VisualBinaryPosition {
return VisualBinaryByYear(decimalYearUTC(date), elements)
}
// VisualBinaryByYear 视双星位置(按小数年) / visual binary position by decimal year.
//
// 返回给定小数年对应的视双星位置角和角距离。
func VisualBinaryByYear(year float64, elements VisualBinaryElements) VisualBinaryPosition {
if !validVisualBinaryElements(year, elements) {
return invalidVisualBinaryPosition(year)
}
meanAnomaly := normalize360Local(360 / elements.PeriodYears * (year - elements.PeriastronYear))
eccentricAnomaly, ok := solveVisualBinaryEccentricAnomaly(meanAnomaly*visualBinaryRad, elements.Eccentricity)
if !ok {
return invalidVisualBinaryPosition(year)
}
sinE, cosE := math.Sincos(eccentricAnomaly)
radius := elements.SemiMajorAxis * (1 - elements.Eccentricity*cosE)
trueAnomaly := math.Atan2(
math.Sqrt(1-elements.Eccentricity*elements.Eccentricity)*sinE,
cosE-elements.Eccentricity,
) * visualBinaryDeg
u := (trueAnomaly + elements.PeriastronArgument) * visualBinaryRad
sinU, cosU := math.Sincos(u)
cosI := math.Cos(elements.Inclination * visualBinaryRad)
thetaMinusNode := math.Atan2(sinU*cosI, cosU) * visualBinaryDeg
positionAngle := normalize360Local(thetaMinusNode + elements.AscendingNode)
separation := radius * math.Hypot(cosU, sinU*cosI)
return VisualBinaryPosition{
Year: year,
MeanAnomaly: meanAnomaly,
EccentricAnomaly: normalize360Local(eccentricAnomaly * visualBinaryDeg),
TrueAnomaly: normalize360Local(trueAnomaly),
Radius: radius,
PositionAngle: positionAngle,
Separation: separation,
}
}
func decimalYearUTC(date time.Time) float64 {
date = date.UTC()
year := date.Year()
start := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(year+1, time.January, 1, 0, 0, 0, 0, time.UTC)
return float64(year) + float64(date.Sub(start))/float64(end.Sub(start))
}
func validVisualBinaryElements(year float64, elements VisualBinaryElements) bool {
if !isFiniteLocal(year) ||
!isFiniteLocal(elements.PeriodYears) ||
!isFiniteLocal(elements.PeriastronYear) ||
!isFiniteLocal(elements.Eccentricity) ||
!isFiniteLocal(elements.SemiMajorAxis) ||
!isFiniteLocal(elements.Inclination) ||
!isFiniteLocal(elements.AscendingNode) ||
!isFiniteLocal(elements.PeriastronArgument) {
return false
}
return elements.PeriodYears > 0 &&
elements.SemiMajorAxis > 0 &&
elements.Eccentricity >= 0 &&
elements.Eccentricity < 1
}
func invalidVisualBinaryPosition(year float64) VisualBinaryPosition {
nan := math.NaN()
return VisualBinaryPosition{
Year: year,
MeanAnomaly: nan,
EccentricAnomaly: nan,
TrueAnomaly: nan,
Radius: nan,
PositionAngle: nan,
Separation: nan,
}
}
func solveVisualBinaryEccentricAnomaly(meanAnomalyRad, eccentricity float64) (float64, bool) {
if !isFiniteLocal(meanAnomalyRad) || !isFiniteLocal(eccentricity) || eccentricity < 0 || eccentricity >= 1 {
return math.NaN(), false
}
if meanAnomalyRad > math.Pi {
meanAnomalyRad -= 2 * math.Pi
} else if meanAnomalyRad < -math.Pi {
meanAnomalyRad += 2 * math.Pi
}
eccentricAnomaly := meanAnomalyRad
if eccentricity >= 0.8 {
eccentricAnomaly = math.Pi
if meanAnomalyRad < 0 {
eccentricAnomaly = -math.Pi
}
}
for i := 0; i < 32; i++ {
sinE, cosE := math.Sincos(eccentricAnomaly)
delta := (eccentricAnomaly - eccentricity*sinE - meanAnomalyRad) / (1 - eccentricity*cosE)
eccentricAnomaly -= delta
if math.Abs(delta) < 1e-14 {
return eccentricAnomaly, true
}
}
return eccentricAnomaly, true
}
func normalize360Local(value float64) float64 {
value = math.Mod(value, 360)
if value < 0 {
value += 360
}
return value
}
func isFiniteLocal(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}