159 lines
5.6 KiB
Go
159 lines
5.6 KiB
Go
|
|
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)
|
|||
|
|
}
|