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