- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息 - 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算 - 新增木星伽利略卫星位置、现象与接触事件计算 - 新增恒星星表、星座判定、自行修正与观测辅助能力 - 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包 - 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算 - 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试 - 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试 - 更新中文和英文 README,补充示例、精度说明、SVG 配图
425 lines
15 KiB
Go
425 lines
15 KiB
Go
package basic
|
||
|
||
import (
|
||
"math"
|
||
|
||
"b612.me/astro/planet"
|
||
. "b612.me/astro/tools"
|
||
)
|
||
|
||
const (
|
||
jupiterGalileanReferenceJD = 2433282.5
|
||
jupiterGalileanLongPeriodShift = 310910.16
|
||
jupiterGalileanMinSolarLPYear = 1150.0
|
||
jupiterGalileanMaxSolarLPYear = 2750.0
|
||
jupiterGalileanEquatorialRadiusKM = 71492.0
|
||
astronomicalUnitKM = 149597870.691
|
||
)
|
||
|
||
var (
|
||
jupiterGalileanBaseMeanLongitudes = [4]float64{3.55155228618240, 1.76932271112347, 0.878207923589328, 0.376486233433828}
|
||
jupiterGalileanMu = [4]float64{2.82489428433814e-07, 2.82483274392893e-07, 2.82498184184723e-07, 2.82492144889909e-07}
|
||
)
|
||
|
||
const (
|
||
jupiterGalileanFrameNode = 6.24950183065715
|
||
jupiterGalileanFrameTilt = 0.445094736497665
|
||
)
|
||
|
||
type jupiterGalileanL1Term struct {
|
||
Amp float64
|
||
Period float64
|
||
Phase float64
|
||
}
|
||
|
||
// JupiterGalileanState 木星伽利略卫星原始状态 / raw Galilean-satellite state.
|
||
//
|
||
// 输入 jd 使用 TT/TDB 对应的儒略日;返回值为 IMCCE L1 理论的木心 J2000 平赤道直角坐标与速度,单位 AU / AU/day。
|
||
// The input jd is a TT/TDB Julian day. Returned coordinates are Jovicentric J2000 mean-equatorial position and velocity from the IMCCE L1 theory, in AU and AU/day.
|
||
type JupiterGalileanState struct {
|
||
X float64
|
||
Y float64
|
||
Z float64
|
||
VX float64
|
||
VY float64
|
||
VZ float64
|
||
}
|
||
|
||
// JupiterGalileanObservation 木星伽利略卫星视位置 / apparent Galilean-satellite geometry.
|
||
//
|
||
// 视位置相对木星中心定义:X 向天球东为正,Y 向天球北为正,Z>0 表示比木星更远、位于盘后。
|
||
// Apparent offsets are relative to Jupiter's center: X is positive to celestial east, Y to celestial north, and Z>0 means farther than Jupiter and behind the disk.
|
||
type JupiterGalileanObservation struct {
|
||
State JupiterGalileanState
|
||
|
||
RA float64
|
||
Dec float64
|
||
Distance float64
|
||
|
||
OffsetXArcsec float64
|
||
OffsetYArcsec float64
|
||
|
||
OffsetXJupiterRadii float64
|
||
OffsetYJupiterRadii float64
|
||
OffsetZJupiterRadii float64
|
||
|
||
InFrontOfJupiter bool
|
||
}
|
||
|
||
// JupiterGalileanSatelliteState 伽利略卫星木心 J2000 状态 / Jovicentric J2000 state of a Galilean satellite.
|
||
//
|
||
// satellite 取 1=Io, 2=Europa, 3=Ganymede, 4=Callisto。jd 为 TT/TDB 对应儒略日。
|
||
// satellite is 1=Io, 2=Europa, 3=Ganymede, 4=Callisto. jd is a TT/TDB Julian day.
|
||
func JupiterGalileanSatelliteState(jd float64, satellite int) JupiterGalileanState {
|
||
if satellite < 1 || satellite > 4 || !isFinite(jd) {
|
||
return invalidJupiterGalileanState()
|
||
}
|
||
et := jd - jupiterGalileanReferenceJD
|
||
includeSolarLongPeriod := jupiterGalileanUseSolarLongPeriod(jd)
|
||
return jupiterGalileanSatelliteStateAtET(et, satellite-1, includeSolarLongPeriod)
|
||
}
|
||
|
||
// JupiterGalileanSatelliteStates 四颗伽利略卫星木心 J2000 状态 / Jovicentric J2000 states of the four Galilean satellites.
|
||
//
|
||
// 返回次序固定为 Io、Europa、Ganymede、Callisto。
|
||
// The returned order is Io, Europa, Ganymede, Callisto.
|
||
func JupiterGalileanSatelliteStates(jd float64) [4]JupiterGalileanState {
|
||
var states [4]JupiterGalileanState
|
||
et := jd - jupiterGalileanReferenceJD
|
||
includeSolarLongPeriod := jupiterGalileanUseSolarLongPeriod(jd)
|
||
for i := range states {
|
||
states[i] = jupiterGalileanSatelliteStateAtET(et, i, includeSolarLongPeriod)
|
||
}
|
||
return states
|
||
}
|
||
|
||
// JupiterGalileanSatelliteObservation 伽利略卫星视位置 / apparent geometry of a Galilean satellite.
|
||
//
|
||
// jd 为 TT/TDB 对应儒略日;返回卫星的天球视赤道坐标,以及相对木星中心的东/北平面偏移。
|
||
// jd is a TT/TDB Julian day. The result contains the satellite's astrometric equatorial coordinates and its east/north sky-plane offsets relative to Jupiter's center.
|
||
func JupiterGalileanSatelliteObservation(jd float64, satellite int) JupiterGalileanObservation {
|
||
if satellite < 1 || satellite > 4 || !isFinite(jd) {
|
||
return invalidJupiterGalileanObservation()
|
||
}
|
||
context := newJupiterGalileanObservationContext(jd)
|
||
return context.observationForSatellite(satellite - 1)
|
||
}
|
||
|
||
// JupiterGalileanSatelliteObservations 四颗伽利略卫星视位置 / apparent geometry of the four Galilean satellites.
|
||
//
|
||
// 返回次序固定为 Io、Europa、Ganymede、Callisto。
|
||
// The returned order is Io, Europa, Ganymede, Callisto.
|
||
func JupiterGalileanSatelliteObservations(jd float64) [4]JupiterGalileanObservation {
|
||
var observations [4]JupiterGalileanObservation
|
||
context := newJupiterGalileanObservationContext(jd)
|
||
for i := range observations {
|
||
observations[i] = context.observationForSatellite(i)
|
||
}
|
||
return observations
|
||
}
|
||
|
||
type jupiterGalileanObservationContext struct {
|
||
jd float64
|
||
targetJD float64
|
||
earthHelioJ2000 Vector3
|
||
jupiterGeoJ2000 Vector3
|
||
jupiterDistance float64
|
||
jupiterLightTime float64
|
||
sunDistanceAU float64
|
||
east Vector3
|
||
north Vector3
|
||
lineOfSight Vector3
|
||
earthDirection Vector3
|
||
sunDirection Vector3
|
||
sunLineOfSight Vector3
|
||
sunEast Vector3
|
||
sunNorth Vector3
|
||
earthMinorRadius float64
|
||
sunMinorRadius float64
|
||
bodyX Vector3
|
||
bodyY Vector3
|
||
bodyZ Vector3
|
||
}
|
||
|
||
func newJupiterGalileanObservationContext(jd float64) jupiterGalileanObservationContext {
|
||
context := jupiterGalileanObservationContext{jd: jd}
|
||
if !isFinite(jd) {
|
||
return context
|
||
}
|
||
context.earthHelioJ2000 = rotateEclipticToEquatorial(earthHeliocentricVectorJ2000(jd), orbitJ2000Obliquity)
|
||
context.jupiterGeoJ2000, context.jupiterLightTime = jupiterAstrometricGeocentricVectorJ2000(jd, context.earthHelioJ2000)
|
||
context.targetJD = jd - context.jupiterLightTime
|
||
context.jupiterDistance = vectorMagnitude(context.jupiterGeoJ2000)
|
||
if context.jupiterDistance == 0 {
|
||
return context
|
||
}
|
||
context.lineOfSight = normalizeVector(context.jupiterGeoJ2000)
|
||
context.earthDirection = Vector3{-context.lineOfSight[0], -context.lineOfSight[1], -context.lineOfSight[2]}
|
||
ra, dec := vectorToRaDec(context.lineOfSight)
|
||
context.east = Vector3{-Sin(ra), Cos(ra), 0}
|
||
context.north = Vector3{-Cos(ra) * Sin(dec), -Sin(ra) * Sin(dec), Cos(dec)}
|
||
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(context.targetJD), orbitJ2000Obliquity)
|
||
context.sunDistanceAU = vectorMagnitude(jupiterHelio)
|
||
context.sunDirection = normalizeVector(Vector3{-jupiterHelio[0], -jupiterHelio[1], -jupiterHelio[2]})
|
||
context.sunLineOfSight = Vector3{-context.sunDirection[0], -context.sunDirection[1], -context.sunDirection[2]}
|
||
sunRA, sunDec := vectorToRaDec(context.sunLineOfSight)
|
||
context.sunEast = Vector3{-Sin(sunRA), Cos(sunRA), 0}
|
||
context.sunNorth = Vector3{-Cos(sunRA) * Sin(sunDec), -Sin(sunRA) * Sin(sunDec), Cos(sunDec)}
|
||
poleRA, poleDec, _ := jupiterPoleRotation(context.targetJD)
|
||
context.bodyZ = raDecToVector(poleRA, poleDec)
|
||
context.bodyX = normalizeVector(Vector3{-math.Sin(poleRA * rad), math.Cos(poleRA * rad), 0})
|
||
context.bodyY = normalizeVector(pxp(context.bodyZ, context.bodyX))
|
||
context.earthMinorRadius = jupiterProjectedMinorRadius(context.earthDirection, context.bodyZ)
|
||
context.sunMinorRadius = jupiterProjectedMinorRadius(context.sunDirection, context.bodyZ)
|
||
return context
|
||
}
|
||
|
||
func (context jupiterGalileanObservationContext) observationForSatellite(index int) JupiterGalileanObservation {
|
||
if index < 0 || index >= 4 || context.jupiterDistance == 0 {
|
||
return invalidJupiterGalileanObservation()
|
||
}
|
||
state, geocentric := jupiterGalileanSatelliteAstrometricGeocentric(index, context.jd, context.jupiterLightTime, context.earthHelioJ2000)
|
||
direction := normalizeVector(geocentric)
|
||
ra, dec := vectorToRaDec(direction)
|
||
distance := vectorMagnitude(geocentric)
|
||
relative := Vector3{
|
||
geocentric[0] - context.jupiterGeoJ2000[0],
|
||
geocentric[1] - context.jupiterGeoJ2000[1],
|
||
geocentric[2] - context.jupiterGeoJ2000[2],
|
||
}
|
||
zAU := vectorDot(relative, context.lineOfSight)
|
||
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
|
||
offsetXRad, offsetYRad := tangentPlaneOffsetAngles(direction, context.lineOfSight, context.east, context.north)
|
||
jupiterSemidiameterArcsec := math.Atan2(radiusAU, context.jupiterDistance) * deg * 3600
|
||
return JupiterGalileanObservation{
|
||
State: state,
|
||
|
||
RA: ra,
|
||
Dec: dec,
|
||
Distance: distance,
|
||
|
||
OffsetXArcsec: offsetXRad * deg * 3600,
|
||
OffsetYArcsec: offsetYRad * deg * 3600,
|
||
|
||
OffsetXJupiterRadii: offsetXRad * deg * 3600 / jupiterSemidiameterArcsec,
|
||
OffsetYJupiterRadii: offsetYRad * deg * 3600 / jupiterSemidiameterArcsec,
|
||
OffsetZJupiterRadii: zAU / radiusAU,
|
||
|
||
InFrontOfJupiter: zAU < 0,
|
||
}
|
||
}
|
||
|
||
func tangentPlaneOffsetAngles(target, center, east, north Vector3) (float64, float64) {
|
||
denominator := vectorDot(target, center)
|
||
return math.Atan2(vectorDot(target, east), denominator), math.Atan2(vectorDot(target, north), denominator)
|
||
}
|
||
|
||
func jupiterGalileanSatelliteAstrometricGeocentric(index int, jd, initialLightTime float64, earthHelioJ2000 Vector3) (JupiterGalileanState, Vector3) {
|
||
lightTime := initialLightTime
|
||
state := JupiterGalileanState{}
|
||
result := Vector3{}
|
||
includeSolarLongPeriod := jupiterGalileanUseSolarLongPeriod(jd)
|
||
for i := 0; i < 8; i++ {
|
||
targetJD := jd - lightTime
|
||
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(targetJD), orbitJ2000Obliquity)
|
||
state = jupiterGalileanSatelliteStateAtET(targetJD-jupiterGalileanReferenceJD, index, includeSolarLongPeriod)
|
||
result = Vector3{
|
||
jupiterHelio[0] + state.X - earthHelioJ2000[0],
|
||
jupiterHelio[1] + state.Y - earthHelioJ2000[1],
|
||
jupiterHelio[2] + state.Z - earthHelioJ2000[2],
|
||
}
|
||
nextLightTime := lightTimeDaysPerAU * vectorMagnitude(result)
|
||
if math.Abs(nextLightTime-lightTime) < 1e-12 {
|
||
break
|
||
}
|
||
lightTime = nextLightTime
|
||
}
|
||
return state, result
|
||
}
|
||
|
||
func jupiterAstrometricGeocentricVectorJ2000(jd float64, earthHelioJ2000 Vector3) (Vector3, float64) {
|
||
lightTime := 0.0
|
||
result := Vector3{}
|
||
for i := 0; i < 8; i++ {
|
||
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(jd-lightTime), orbitJ2000Obliquity)
|
||
result = Vector3{
|
||
jupiterHelio[0] - earthHelioJ2000[0],
|
||
jupiterHelio[1] - earthHelioJ2000[1],
|
||
jupiterHelio[2] - earthHelioJ2000[2],
|
||
}
|
||
nextLightTime := lightTimeDaysPerAU * vectorMagnitude(result)
|
||
if math.Abs(nextLightTime-lightTime) < 1e-12 {
|
||
return result, nextLightTime
|
||
}
|
||
lightTime = nextLightTime
|
||
}
|
||
return result, lightTime
|
||
}
|
||
|
||
func jupiterHeliocentricVectorJ2000(jd float64) Vector3 {
|
||
return eclipticVectorAtReferenceEpoch(
|
||
eclipticCartesian(
|
||
planet.WherePlanet(4, 0, jd),
|
||
planet.WherePlanet(4, 1, jd),
|
||
planet.WherePlanet(4, 2, jd),
|
||
),
|
||
jd,
|
||
orbitReferenceJD,
|
||
)
|
||
}
|
||
|
||
func jupiterGalileanSatelliteStateAtET(et float64, index int, includeSolarLongPeriod bool) JupiterGalileanState {
|
||
elements := jupiterGalileanElementsAtET(et, index, includeSolarLongPeriod)
|
||
pv := jupiterGalileanElementsToPV(jupiterGalileanMu[index], elements)
|
||
cosNode, sinNode := math.Cos(jupiterGalileanFrameNode), math.Sin(jupiterGalileanFrameNode)
|
||
cosTilt, sinTilt := math.Cos(jupiterGalileanFrameTilt), math.Sin(jupiterGalileanFrameTilt)
|
||
return JupiterGalileanState{
|
||
X: pv[0]*cosNode - pv[1]*sinNode*cosTilt + pv[2]*sinTilt*sinNode,
|
||
Y: pv[0]*sinNode + pv[1]*cosNode*cosTilt - pv[2]*sinTilt*cosNode,
|
||
Z: pv[1]*sinTilt + pv[2]*cosTilt,
|
||
VX: pv[3]*cosNode - pv[4]*sinNode*cosTilt + pv[5]*sinTilt*sinNode,
|
||
VY: pv[3]*sinNode + pv[4]*cosNode*cosTilt - pv[5]*sinTilt*cosNode,
|
||
VZ: pv[4]*sinTilt + pv[5]*cosTilt,
|
||
}
|
||
}
|
||
|
||
type jupiterGalileanElements struct {
|
||
A float64
|
||
L float64
|
||
K float64
|
||
H float64
|
||
Q float64
|
||
P float64
|
||
}
|
||
|
||
func jupiterGalileanElementsAtET(et float64, index int, includeSolarLongPeriod bool) jupiterGalileanElements {
|
||
longPeriod := jupiterGalileanEvaluateSeries(jupiterGalileanL1LongPeriodTerms[index], et+jupiterGalileanLongPeriodShift, et, includeSolarLongPeriod, index)
|
||
crossPeriod := jupiterGalileanEvaluateSeries(jupiterGalileanL1CrossPeriodTerms[index], et, et, false, index)
|
||
combined := jupiterGalileanElements{
|
||
A: longPeriod.A + crossPeriod.A,
|
||
L: longPeriod.L + crossPeriod.L + jupiterGalileanBaseMeanLongitudes[index]*et,
|
||
K: longPeriod.K + crossPeriod.K,
|
||
H: longPeriod.H + crossPeriod.H,
|
||
Q: longPeriod.Q + crossPeriod.Q,
|
||
P: longPeriod.P + crossPeriod.P,
|
||
}
|
||
combined.L = math.Atan2(math.Sin(combined.L), math.Cos(combined.L))
|
||
if combined.L < 0 {
|
||
combined.L += 2 * math.Pi
|
||
}
|
||
return combined
|
||
}
|
||
|
||
func jupiterGalileanEvaluateSeries(blocks [4][]jupiterGalileanL1Term, angleTime, et float64, includeSolarLongPeriod bool, index int) jupiterGalileanElements {
|
||
vals := [5]float64{}
|
||
if includeSolarLongPeriod {
|
||
x := (et/365.25 - 0.5*(812.721806990360-819.727638594856)) / (0.5 * (812.721806990360 - -819.727638594856))
|
||
tn := [9]float64{1, x}
|
||
for i := 2; i < len(tn); i++ {
|
||
tn[i] = 2*x*tn[i-1] - tn[i-2]
|
||
}
|
||
for variable := 0; variable < len(vals); variable++ {
|
||
sum := 0.0
|
||
for term := 0; term < len(tn); term++ {
|
||
sum += jupiterGalileanL1Chebyshev[index][variable][term] * tn[term]
|
||
}
|
||
vals[variable] = sum - 0.5*jupiterGalileanL1Chebyshev[index][variable][0]
|
||
}
|
||
}
|
||
|
||
result := jupiterGalileanElements{}
|
||
for blockIndex, terms := range blocks {
|
||
realPart, imagPart := 0.0, 0.0
|
||
for _, term := range terms {
|
||
angle := term.Phase
|
||
if term.Period != 0 {
|
||
angle += 2 * math.Pi * angleTime / term.Period
|
||
}
|
||
realPart += term.Amp * math.Cos(angle)
|
||
imagPart += term.Amp * math.Sin(angle)
|
||
}
|
||
switch blockIndex {
|
||
case 0:
|
||
result.A = realPart
|
||
case 1:
|
||
result.L = realPart + vals[0]
|
||
case 2:
|
||
result.K = realPart + vals[1]
|
||
result.H = imagPart + vals[2]
|
||
case 3:
|
||
result.Q = realPart + vals[3]
|
||
result.P = imagPart + vals[4]
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
func jupiterGalileanElementsToPV(mu float64, elements jupiterGalileanElements) [6]float64 {
|
||
k := elements.K
|
||
h := elements.H
|
||
q := elements.Q
|
||
p := elements.P
|
||
a := elements.A
|
||
al := elements.L
|
||
an := math.Sqrt(mu / math.Pow(a, 3))
|
||
ee := al + k*math.Sin(al) - h*math.Cos(al)
|
||
for {
|
||
ce := math.Cos(ee)
|
||
se := math.Sin(ee)
|
||
de := (al - ee + k*se - h*ce) / (1 - k*ce - h*se)
|
||
ee += de
|
||
if math.Abs(de) < 1e-12 {
|
||
break
|
||
}
|
||
}
|
||
ce := math.Cos(ee)
|
||
se := math.Sin(ee)
|
||
dle := h*ce - k*se
|
||
rsam1 := -k*ce - h*se
|
||
asr := 1 / (1 + rsam1)
|
||
phi := math.Sqrt(1 - k*k - h*h)
|
||
psi := 1 / (1 + phi)
|
||
x1 := a * (ce - k - psi*h*dle)
|
||
y1 := a * (se - h + psi*k*dle)
|
||
vx1 := an * asr * a * (-se - psi*h*rsam1)
|
||
vy1 := an * asr * a * (ce + psi*k*rsam1)
|
||
f2 := 2 * math.Sqrt(1-q*q-p*p)
|
||
p2 := 1 - 2*p*p
|
||
q2 := 1 - 2*q*q
|
||
pq := 2 * p * q
|
||
return [6]float64{
|
||
x1*p2 + y1*pq,
|
||
x1*pq + y1*q2,
|
||
(q*y1 - x1*p) * f2,
|
||
vx1*p2 + vy1*pq,
|
||
vx1*pq + vy1*q2,
|
||
(q*vy1 - vx1*p) * f2,
|
||
}
|
||
}
|
||
|
||
func jupiterGalileanUseSolarLongPeriod(jd float64) bool {
|
||
year := 2000.0 + (jd-2451545.0)/365.25
|
||
return year >= jupiterGalileanMinSolarLPYear && year <= jupiterGalileanMaxSolarLPYear
|
||
}
|
||
|
||
func invalidJupiterGalileanState() JupiterGalileanState {
|
||
nan := math.NaN()
|
||
return JupiterGalileanState{X: nan, Y: nan, Z: nan, VX: nan, VY: nan, VZ: nan}
|
||
}
|
||
|
||
func invalidJupiterGalileanObservation() JupiterGalileanObservation {
|
||
nan := math.NaN()
|
||
return JupiterGalileanObservation{
|
||
State: invalidJupiterGalileanState(),
|
||
RA: nan,
|
||
Dec: nan,
|
||
Distance: nan,
|
||
OffsetXArcsec: nan,
|
||
OffsetYArcsec: nan,
|
||
OffsetXJupiterRadii: nan,
|
||
OffsetYJupiterRadii: nan,
|
||
OffsetZJupiterRadii: nan,
|
||
InFrontOfJupiter: false,
|
||
}
|
||
}
|