Files
astro/basic/jupiter_satellite_phenomena.go
T
b612 044f2f4381 feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
2026-10-15 23:54:15 +08:00

204 lines
7.4 KiB
Go

package basic
import (
"math"
)
const solarRadiusAU = 695700.0 / astronomicalUnitKM
// JupiterGalileanPhenomenon 木星伽利略卫星瞬时现象 / instantaneous Galilean-satellite phenomena.
//
// Transit 表示卫星本体在木星盘前;Occultation 表示卫星在木星盘后被掩蔽;Eclipse 表示卫星落入木星本影;ShadowTransit 表示卫星影心落在可见木星盘面上。
// Transit means the satellite itself is in front of Jupiter's disk; Occultation means it is hidden behind the disk; Eclipse means the satellite lies in Jupiter's umbra; ShadowTransit means the center of the satellite shadow falls on the visible Jovian disk.
type JupiterGalileanPhenomenon struct {
Transit bool
Occultation bool
Eclipse bool
ShadowTransit bool
ShadowOffsetXArcsec float64
ShadowOffsetYArcsec float64
ShadowOffsetXJupiterRadii float64
ShadowOffsetYJupiterRadii float64
}
// JupiterGalileanSatellitePhenomenon 单颗伽利略卫星瞬时现象 / instantaneous phenomena of one Galilean satellite.
func JupiterGalileanSatellitePhenomenon(jde float64, satellite int) JupiterGalileanPhenomenon {
if satellite < 1 || satellite > 4 || !isFinite(jde) {
return invalidJupiterGalileanPhenomenon()
}
context := newJupiterGalileanObservationContext(jde)
return context.phenomenonForSatellite(satellite - 1)
}
// JupiterGalileanSatellitePhenomenaWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterGalileanSatellitePhenomenaWithInstance(jde float64, inst *Instance) [4]JupiterGalileanPhenomenon {
var phenomena [4]JupiterGalileanPhenomenon
context := newJupiterGalileanObservationContext(jde, inst)
for i := range phenomena {
phenomena[i] = context.phenomenonForSatellite(i)
}
return phenomena
}
func (context jupiterGalileanObservationContext) phenomenonForSatellite(index int) JupiterGalileanPhenomenon {
if index < 0 || index >= 4 || context.jupiterDistance == 0 {
return invalidJupiterGalileanPhenomenon()
}
observation := context.observationForSatellite(index)
stateVector := Vector3{observation.State.X, observation.State.Y, observation.State.Z}
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
xEarth := observation.OffsetXJupiterRadii
yEarth := observation.OffsetYJupiterRadii
onEarthDisk := ellipseInside(xEarth, yEarth, 1, context.earthMinorRadius)
xSunAU := vectorDot(stateVector, context.sunEast)
ySunAU := vectorDot(stateVector, context.sunNorth)
zSunAU := vectorDot(stateVector, context.sunLineOfSight)
xSun := xSunAU / radiusAU
ySun := ySunAU / radiusAU
umbraScale := jupiterUmbraScale(zSunAU, context.sunDistanceAU)
eclipse := false
if zSunAU > 0 && umbraScale > 0 {
eclipse = ellipseInside(xSun, ySun, umbraScale, context.sunMinorRadius*umbraScale)
}
shadowTransit, shadowXAU, shadowYAU := context.shadowTransitFor(stateVector)
phenomenon := JupiterGalileanPhenomenon{
Transit: onEarthDisk && observation.InFrontOfJupiter,
Occultation: onEarthDisk && !observation.InFrontOfJupiter,
Eclipse: eclipse,
ShadowTransit: shadowTransit,
ShadowOffsetXArcsec: math.NaN(),
ShadowOffsetYArcsec: math.NaN(),
ShadowOffsetXJupiterRadii: math.NaN(),
ShadowOffsetYJupiterRadii: math.NaN(),
}
if shadowTransit {
phenomenon.ShadowOffsetXArcsec = math.Atan2(shadowXAU, context.jupiterDistance) * deg * 3600
phenomenon.ShadowOffsetYArcsec = math.Atan2(shadowYAU, context.jupiterDistance) * deg * 3600
phenomenon.ShadowOffsetXJupiterRadii = shadowXAU / radiusAU
phenomenon.ShadowOffsetYJupiterRadii = shadowYAU / radiusAU
}
return phenomenon
}
func (context jupiterGalileanObservationContext) shadowTransitFor(stateVector Vector3) (bool, float64, float64) {
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
satelliteBody := context.toBodyCoordinates(stateVector)
satelliteBody = Vector3{satelliteBody[0] / radiusAU, satelliteBody[1] / radiusAU, satelliteBody[2] / radiusAU}
directionBody := context.toBodyCoordinates(context.sunLineOfSight)
intersectionBody, ok := ellipsoidRayIntersection(satelliteBody, directionBody, jupiterPolarRadiusRatio())
if !ok {
return false, 0, 0
}
normalBody := Vector3{intersectionBody[0], intersectionBody[1], intersectionBody[2] / (jupiterPolarRadiusRatio() * jupiterPolarRadiusRatio())}
earthBody := context.toBodyCoordinates(context.earthDirection)
if vectorDot(normalBody, earthBody) <= 0 {
return false, 0, 0
}
intersection := context.fromBodyCoordinates(Vector3{
intersectionBody[0] * radiusAU,
intersectionBody[1] * radiusAU,
intersectionBody[2] * radiusAU,
})
xAU := vectorDot(intersection, context.east)
yAU := vectorDot(intersection, context.north)
x := xAU / radiusAU
y := yAU / radiusAU
if !ellipseInside(x, y, 1, context.earthMinorRadius) {
return false, 0, 0
}
return true, xAU, yAU
}
func (context jupiterGalileanObservationContext) toBodyCoordinates(vector Vector3) Vector3 {
return Vector3{
vectorDot(vector, context.bodyX),
vectorDot(vector, context.bodyY),
vectorDot(vector, context.bodyZ),
}
}
func (context jupiterGalileanObservationContext) fromBodyCoordinates(vector Vector3) Vector3 {
return Vector3{
context.bodyX[0]*vector[0] + context.bodyY[0]*vector[1] + context.bodyZ[0]*vector[2],
context.bodyX[1]*vector[0] + context.bodyY[1]*vector[1] + context.bodyZ[1]*vector[2],
context.bodyX[2]*vector[0] + context.bodyY[2]*vector[1] + context.bodyZ[2]*vector[2],
}
}
func jupiterProjectedMinorRadius(direction, pole Vector3) float64 {
sinBeta := vectorDot(direction, pole)
cos2Beta := 1 - sinBeta*sinBeta
if cos2Beta < 0 {
cos2Beta = 0
}
ratio := jupiterPolarRadiusRatio()
return math.Sqrt(sinBeta*sinBeta + ratio*ratio*cos2Beta)
}
func jupiterPolarRadiusRatio() float64 {
return jupiterPhysicalModel.polarRadius / jupiterPhysicalModel.equatorialRadius
}
func ellipseInside(x, y, major, minor float64) bool {
if major <= 0 || minor <= 0 {
return false
}
return (x*x)/(major*major)+(y*y)/(minor*minor) <= 1+1e-12
}
func ellipsoidRayIntersection(origin, direction Vector3, polarRatio float64) (Vector3, bool) {
invPolar2 := 1 / (polarRatio * polarRatio)
a := direction[0]*direction[0] + direction[1]*direction[1] + direction[2]*direction[2]*invPolar2
b := 2 * (origin[0]*direction[0] + origin[1]*direction[1] + origin[2]*direction[2]*invPolar2)
c := origin[0]*origin[0] + origin[1]*origin[1] + origin[2]*origin[2]*invPolar2 - 1
discriminant := b*b - 4*a*c
if discriminant < 0 {
return Vector3{}, false
}
sqrtDiscriminant := math.Sqrt(discriminant)
t1 := (-b - sqrtDiscriminant) / (2 * a)
t2 := (-b + sqrtDiscriminant) / (2 * a)
t := math.Inf(1)
if t1 > 0 {
t = t1
}
if t2 > 0 && t2 < t {
t = t2
}
if !isFinite(t) {
return Vector3{}, false
}
return Vector3{
origin[0] + t*direction[0],
origin[1] + t*direction[1],
origin[2] + t*direction[2],
}, true
}
func jupiterUmbraScale(distanceBehindAU, sunDistanceAU float64) float64 {
if distanceBehindAU <= 0 || sunDistanceAU <= 0 {
return 0
}
jupiterRadiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
umbraLength := jupiterRadiusAU * sunDistanceAU / (solarRadiusAU - jupiterRadiusAU)
if umbraLength <= 0 {
return 0
}
return 1 - distanceBehindAU/umbraLength
}
func invalidJupiterGalileanPhenomenon() JupiterGalileanPhenomenon {
nan := math.NaN()
return JupiterGalileanPhenomenon{
ShadowOffsetXArcsec: nan,
ShadowOffsetYArcsec: nan,
ShadowOffsetXJupiterRadii: nan,
ShadowOffsetYJupiterRadii: nan,
}
}