astro/orbit/visual_binary.go
starainrt c8dd777a7b
docs: 统一公开 API 的中英双语注释
- 补齐公开接口说明段的英文描述,保持签名注释和详细说明均为中英双语结构
- 规范农历、坐标、公式、轨道、日晷、太阳、恒星及行星事件等 API 的注释口径
2026-05-27 16:08:11 +08:00

163 lines
5.9 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 视双星轨道要素 / visual-binary orbital elements.
//
// 采用《天文算法》第 55 章的经典口径。
// Uses the classical convention described in Chapter 55 of Astronomical Algorithms.
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 视双星在天球上的计算结果 / computed sky-plane position of a visual binary.
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.
//
// 返回给定小数年对应的视双星位置角和角距离。
// Returns the position angle and apparent separation for the supplied 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)
}