astro/orbit/visual_binary.go

159 lines
5.6 KiB
Go
Raw Normal View History

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)
}