2026-05-01 22:38:44 +08:00
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.
2026-09-23 18:55:12 +08:00
func JupiterGalileanSatellitePhenomenon ( jde float64 , satellite int ) JupiterGalileanPhenomenon {
if satellite < 1 || satellite > 4 || ! isFinite ( jde ) {
2026-05-01 22:38:44 +08:00
return invalidJupiterGalileanPhenomenon ()
}
2026-09-23 18:55:12 +08:00
context := newJupiterGalileanObservationContext ( jde )
2026-05-01 22:38:44 +08:00
return context . phenomenonForSatellite ( satellite - 1 )
}
// JupiterGalileanSatellitePhenomena 四颗伽利略卫星瞬时现象 / instantaneous phenomena of the four Galilean satellites.
2026-09-23 18:55:12 +08:00
func JupiterGalileanSatellitePhenomena ( jde float64 ) [ 4 ] JupiterGalileanPhenomenon {
2026-05-01 22:38:44 +08:00
var phenomena [ 4 ] JupiterGalileanPhenomenon
2026-09-23 18:55:12 +08:00
context := newJupiterGalileanObservationContext ( jde )
2026-05-01 22:38:44 +08:00
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 ,
}
}