astro/basic/jupiter_satellite_contact_events.go

828 lines
30 KiB
Go
Raw Normal View History

package basic
import "math"
const (
jupiterGalileanContactBracketStepDays = 2.0 / 1440.0
jupiterGalileanContactBracketSpanDays = 10.0 / 24.0
)
// JupiterGalileanPhenomenonContactPhase 接触阶段 / contact phase.
type JupiterGalileanPhenomenonContactPhase string
const (
// JupiterGalileanDisappearanceContact 初亏/初入接触阶段 / disappearance ingress contact.
JupiterGalileanDisappearanceContact JupiterGalileanPhenomenonContactPhase = "disappearance"
// JupiterGalileanReappearanceContact 复圆/复出接触阶段 / reappearance egress contact.
JupiterGalileanReappearanceContact JupiterGalileanPhenomenonContactPhase = "reappearance"
)
// JupiterGalileanPhenomenonContact 伽利略卫星接触窗口 / Galilean-satellite contact window.
//
// Start/End 表示有限圆盘或有限影斑开始/结束接触的时刻ModelCrossing 表示这套连续接触模型下,
// 零半径参考点穿越边界的时刻。
// Start/End mark the beginning/end of the finite-disk or finite-shadow contact interval.
// ModelCrossing is the zero-radius boundary crossing in this continuous contact model.
type JupiterGalileanPhenomenonContact struct {
Valid bool
Phase JupiterGalileanPhenomenonContactPhase
Start float64
ModelCrossing float64
End float64
}
// JupiterGalileanPhenomenonContactEvent IMCCE 风格的 D/F 接触事件 / IMCCE-style D/F contact event.
//
// 与 `JupiterGalileanPhenomenonEvent` 不同,这里返回的是有限圆盘/有限影斑的初亏与复圆接触窗口;
// 现有整场事件 API 返回的则是零半径几何模型处于 active 状态的整段区间。
// 对 `shadow_transit`,这里按 IMCCE 的影凌语义处理:先用半影/本影边界求出部分相持续时间,
// 再把这段持续时间中心放在旧 `shadow_transit` API 的影轴过盘时刻上。
// Unlike `JupiterGalileanPhenomenonEvent`, this returns the finite-disk / finite-shadow D/F contact windows.
// The existing full-event API returns the whole active interval of the zero-radius geometric model.
// For `shadow_transit`, the partial-phase duration comes from penumbra/umbra boundaries,
// while the reported D/F time is centered on the shadow-axis limb crossing from the existing full-event model.
type JupiterGalileanPhenomenonContactEvent struct {
Valid bool
Satellite int
Type JupiterGalileanPhenomenonType
Disappearance JupiterGalileanPhenomenonContact
Greatest float64
Reappearance JupiterGalileanPhenomenonContact
GreatestPhenomenon JupiterGalileanPhenomenon
}
type jupiterGalileanContactGeometry struct {
signedDistance float64
effectiveRadius float64
}
// LastJupiterGalileanPhenomenonContactEvent 上一次 IMCCE 风格接触事件 / previous IMCCE-style contact event.
func LastJupiterGalileanPhenomenonContactEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonContactEvent {
return jupiterGalileanPhenomenonContactEventFromEvent(
LastJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType),
)
}
// NextJupiterGalileanPhenomenonContactEvent 下一次 IMCCE 风格接触事件 / next IMCCE-style contact event.
func NextJupiterGalileanPhenomenonContactEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonContactEvent {
return jupiterGalileanPhenomenonContactEventFromEvent(
NextJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType),
)
}
// ClosestJupiterGalileanPhenomenonContactEvent 最近一次 IMCCE 风格接触事件 / closest IMCCE-style contact event.
func ClosestJupiterGalileanPhenomenonContactEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonContactEvent {
return jupiterGalileanPhenomenonContactEventFromEvent(
ClosestJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType),
)
}
func jupiterGalileanPhenomenonContactEventFromEvent(event JupiterGalileanPhenomenonEvent) JupiterGalileanPhenomenonContactEvent {
if !event.Valid {
return invalidJupiterGalileanPhenomenonContactEvent()
}
var (
disappearance JupiterGalileanPhenomenonContact
reappearance JupiterGalileanPhenomenonContact
ok bool
)
if event.Type == JupiterGalileanShadowTransit {
disappearance, reappearance, ok = refineJupiterGalileanShadowContactPair(event)
} else if event.Type == JupiterGalileanEclipse {
disappearance, reappearance, ok = refineJupiterGalileanEclipseContactPair(event)
} else {
disappearance, reappearance, ok = refineJupiterGalileanContactPair(event.Greatest, event.Satellite, event.Type)
}
if !ok {
return invalidJupiterGalileanPhenomenonContactEvent()
}
return JupiterGalileanPhenomenonContactEvent{
Valid: true,
Satellite: event.Satellite,
Type: event.Type,
Disappearance: disappearance,
Greatest: event.Greatest,
Reappearance: reappearance,
GreatestPhenomenon: event.GreatestPhenomenon,
}
}
func refineJupiterGalileanContactPair(
greatestJD float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
signedDistance := func(jd float64) float64 {
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType)
if !ok {
return math.NaN()
}
return geometry.signedDistance
}
disappearanceStartTarget := func(jd float64) float64 {
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType)
if !ok {
return math.NaN()
}
return geometry.signedDistance - geometry.effectiveRadius
}
insideTarget := func(jd float64) float64 {
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType)
if !ok {
return math.NaN()
}
return geometry.signedDistance + geometry.effectiveRadius
}
disappearanceModel, ok := refineJupiterGalileanContactRoot(greatestJD, -1, signedDistance)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
reappearanceModel, ok := refineJupiterGalileanContactRoot(greatestJD, 1, signedDistance)
if !ok || reappearanceModel <= disappearanceModel {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
disappearanceStart, ok := refineJupiterGalileanContactRoot(disappearanceModel, -1, disappearanceStartTarget)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
disappearanceEnd, ok := refineJupiterGalileanContactRoot(disappearanceModel, 1, insideTarget)
if !ok || disappearanceEnd <= disappearanceStart {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
reappearanceStart, ok := refineJupiterGalileanContactRoot(reappearanceModel, -1, insideTarget)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
reappearanceEnd, ok := refineJupiterGalileanContactRoot(reappearanceModel, 1, disappearanceStartTarget)
if !ok || reappearanceEnd <= reappearanceStart {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
return JupiterGalileanPhenomenonContact{
Valid: true,
Phase: JupiterGalileanDisappearanceContact,
Start: disappearanceStart,
ModelCrossing: disappearanceModel,
End: disappearanceEnd,
}, JupiterGalileanPhenomenonContact{
Valid: true,
Phase: JupiterGalileanReappearanceContact,
Start: reappearanceStart,
ModelCrossing: reappearanceModel,
End: reappearanceEnd,
}, true
}
func refineJupiterGalileanEclipseContactPair(
event JupiterGalileanPhenomenonEvent,
) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
satelliteRadius := jupiterGalileanSatelliteRadiusJupiterRadii(event.Satellite)
if !isFinite(satelliteRadius) || satelliteRadius <= 0 {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
// IMCCE 的 EC.D/EC.F 更接近“目视消失/重现”而不是纯几何接触:
// D 相用半影入段近似F 相用本影/半影出段的中点近似。
penumbraOuterTarget := func(jd float64) float64 {
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, true)
if !ok {
return math.NaN()
}
return signedDistance - satelliteRadius
}
penumbraInnerTarget := func(jd float64) float64 {
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, true)
if !ok {
return math.NaN()
}
return signedDistance + satelliteRadius
}
umbraInnerTarget := func(jd float64) float64 {
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, false)
if !ok {
return math.NaN()
}
return signedDistance + satelliteRadius
}
umbraOuterTarget := func(jd float64) float64 {
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, false)
if !ok {
return math.NaN()
}
return signedDistance - satelliteRadius
}
disappearanceStart, ok := refineJupiterGalileanContactRoot(event.Start, -1, penumbraOuterTarget)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
disappearanceEnd, ok := refineJupiterGalileanContactRoot(event.Start, 1, penumbraInnerTarget)
if !ok || disappearanceEnd <= disappearanceStart {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
reappearanceUmbraStart, ok := refineJupiterGalileanContactRoot(event.End, -1, umbraInnerTarget)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
reappearancePenumbraStart, ok := refineJupiterGalileanContactRoot(event.End, -1, penumbraInnerTarget)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
reappearanceStart := (reappearanceUmbraStart + reappearancePenumbraStart) / 2
reappearanceUmbraEnd, ok := refineJupiterGalileanContactRoot(event.End, 1, umbraOuterTarget)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
reappearancePenumbraEnd, ok := refineJupiterGalileanContactRoot(event.End, 1, penumbraOuterTarget)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
reappearanceEnd := (reappearanceUmbraEnd + reappearancePenumbraEnd) / 2
if !ok || reappearanceEnd <= reappearanceStart {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
return JupiterGalileanPhenomenonContact{
Valid: true,
Phase: JupiterGalileanDisappearanceContact,
Start: disappearanceStart,
ModelCrossing: event.Start,
End: disappearanceEnd,
}, JupiterGalileanPhenomenonContact{
Valid: true,
Phase: JupiterGalileanReappearanceContact,
Start: reappearanceStart,
ModelCrossing: event.End,
End: reappearanceEnd,
}, true
}
func refineJupiterGalileanShadowContactPair(
event JupiterGalileanPhenomenonEvent,
) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
penumbraMetric := func(jd float64) float64 {
value, ok := jupiterGalileanShadowLimbMetricAt(jd, event.Satellite, true)
if !ok {
return math.NaN()
}
return value
}
umbraMetric := func(jd float64) float64 {
value, ok := jupiterGalileanShadowLimbMetricAt(jd, event.Satellite, false)
if !ok {
return math.NaN()
}
return value
}
penumbraSeedStartJD, penumbraSeedStartValue, ok := findJupiterGalileanNegativeMetricSeed(event.Start, penumbraMetric)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
disappearanceStart, ok := refineJupiterGalileanNegativeWindowRoot(penumbraSeedStartJD, penumbraSeedStartValue, -1, penumbraMetric)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
umbraSeedStartJD, umbraSeedStartValue, ok := findJupiterGalileanNegativeMetricSeed(event.Start, umbraMetric)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
disappearanceUmbraIn, ok := refineJupiterGalileanNegativeWindowRoot(umbraSeedStartJD, umbraSeedStartValue, 1, umbraMetric)
if !ok || disappearanceUmbraIn <= disappearanceStart {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
umbraSeedEndJD, umbraSeedEndValue, ok := findJupiterGalileanNegativeMetricSeed(event.End, umbraMetric)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
reappearanceUmbraOut, ok := refineJupiterGalileanNegativeWindowRoot(umbraSeedEndJD, umbraSeedEndValue, -1, umbraMetric)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
penumbraSeedEndJD, penumbraSeedEndValue, ok := findJupiterGalileanNegativeMetricSeed(event.End, penumbraMetric)
if !ok {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
reappearancePenumbraOut, ok := refineJupiterGalileanNegativeWindowRoot(penumbraSeedEndJD, penumbraSeedEndValue, 1, penumbraMetric)
if !ok || reappearancePenumbraOut <= reappearanceUmbraOut {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
disappearanceDuration := disappearanceUmbraIn - disappearanceStart
reappearanceDuration := reappearancePenumbraOut - reappearanceUmbraOut
if disappearanceDuration <= 0 || reappearanceDuration <= 0 {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
}
disappearanceCenteredStart := event.Start - disappearanceDuration/2
disappearanceCenteredEnd := event.Start + disappearanceDuration/2
reappearanceCenteredStart := event.End - reappearanceDuration/2
reappearanceCenteredEnd := event.End + reappearanceDuration/2
return JupiterGalileanPhenomenonContact{
Valid: true,
Phase: JupiterGalileanDisappearanceContact,
Start: disappearanceCenteredStart,
ModelCrossing: event.Start,
End: disappearanceCenteredEnd,
}, JupiterGalileanPhenomenonContact{
Valid: true,
Phase: JupiterGalileanReappearanceContact,
Start: reappearanceCenteredStart,
ModelCrossing: event.End,
End: reappearanceCenteredEnd,
}, true
}
func refineJupiterGalileanNegativeMetricWindow(
seedJD float64,
metric func(jd float64) float64,
) (float64, float64, bool) {
activeJD, activeValue, ok := findJupiterGalileanNegativeMetricSeed(seedJD, metric)
if !ok {
return math.NaN(), math.NaN(), false
}
start, ok := refineJupiterGalileanNegativeWindowRoot(activeJD, activeValue, -1, metric)
if !ok {
return math.NaN(), math.NaN(), false
}
end, ok := refineJupiterGalileanNegativeWindowRoot(activeJD, activeValue, 1, metric)
if !ok {
return math.NaN(), math.NaN(), false
}
return start, end, true
}
func findJupiterGalileanNegativeMetricSeed(
seedJD float64,
metric func(jd float64) float64,
) (float64, float64, bool) {
value := metric(seedJD)
if isFinite(value) && value < 0 {
return seedJD, value, true
}
step := jupiterGalileanContactBracketStepDays
maxSteps := int(math.Ceil(jupiterGalileanContactBracketSpanDays / step))
for i := 1; i <= maxSteps; i++ {
for _, direction := range []float64{-1, 1} {
candidateJD := seedJD + direction*step*float64(i)
candidateValue := metric(candidateJD)
if isFinite(candidateValue) && candidateValue < 0 {
return candidateJD, candidateValue, true
}
}
}
return math.NaN(), math.NaN(), false
}
func refineJupiterGalileanNegativeWindowRoot(
activeJD, activeValue float64,
direction int,
metric func(jd float64) float64,
) (float64, bool) {
currentJD := activeJD
currentValue := activeValue
step := jupiterGalileanContactBracketStepDays
maxSteps := int(math.Ceil(jupiterGalileanContactBracketSpanDays / step))
for i := 1; i <= maxSteps; i++ {
candidateJD := currentJD + float64(direction)*step
candidateValue := metric(candidateJD)
if !isFinite(candidateValue) {
currentJD = candidateJD
currentValue = math.Inf(1)
continue
}
if candidateValue >= 0 {
return bisectJupiterGalileanContactRoot(currentJD, currentValue, candidateJD, candidateValue, metric)
}
currentJD = candidateJD
currentValue = candidateValue
}
return math.NaN(), false
}
func refineJupiterGalileanContactRoot(
modelCrossingJD float64,
direction int,
target func(jd float64) float64,
) (float64, bool) {
if direction != -1 && direction != 1 {
return math.NaN(), false
}
modelValue := target(modelCrossingJD)
if !isFinite(modelValue) {
return math.NaN(), false
}
step := jupiterGalileanContactBracketStepDays
maxSteps := int(math.Ceil(jupiterGalileanContactBracketSpanDays / step))
nearJD := modelCrossingJD
nearValue := modelValue
for i := 1; i <= maxSteps; i++ {
farJD := modelCrossingJD + float64(direction)*step*float64(i)
farValue := target(farJD)
if !isFinite(farValue) {
continue
}
if nearValue == 0 {
return nearJD, true
}
if farValue == 0 {
return farJD, true
}
if nearValue*farValue < 0 {
return bisectJupiterGalileanContactRoot(nearJD, nearValue, farJD, farValue, target)
}
nearJD = farJD
nearValue = farValue
}
return math.NaN(), false
}
func bisectJupiterGalileanContactRoot(
jd1, value1, jd2, value2 float64,
target func(jd float64) float64,
) (float64, bool) {
leftJD := jd1
rightJD := jd2
leftValue := value1
rightValue := value2
if rightJD < leftJD {
leftJD, rightJD = rightJD, leftJD
leftValue, rightValue = rightValue, leftValue
}
if !isFinite(leftValue) || !isFinite(rightValue) || leftValue*rightValue > 0 {
return math.NaN(), false
}
for i := 0; i < 80 && rightJD-leftJD > jupiterGalileanEventEpsilonDays; i++ {
midJD := (leftJD + rightJD) / 2
midValue := target(midJD)
if !isFinite(midValue) {
return math.NaN(), false
}
if midValue == 0 {
return midJD, true
}
if leftValue*midValue <= 0 {
rightJD = midJD
rightValue = midValue
} else {
leftJD = midJD
leftValue = midValue
}
}
return (leftJD + rightJD) / 2, true
}
func jupiterGalileanContactGeometryAt(
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) (jupiterGalileanContactGeometry, bool) {
if !isFinite(jd) || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
return jupiterGalileanContactGeometry{}, false
}
evaluationJD := TD2UT(jd, true)
context := newJupiterGalileanObservationContext(evaluationJD)
if context.jupiterDistance == 0 {
return jupiterGalileanContactGeometry{}, false
}
index := satellite - 1
observation := context.observationForSatellite(index)
stateVector := Vector3{observation.State.X, observation.State.Y, observation.State.Z}
satelliteRadius := jupiterGalileanSatelliteRadiusJupiterRadii(satellite)
if !isFinite(satelliteRadius) || satelliteRadius <= 0 {
return jupiterGalileanContactGeometry{}, false
}
switch phenomenonType {
case JupiterGalileanTransit, JupiterGalileanOccultation:
return jupiterGalileanContactGeometry{
signedDistance: ellipseSignedDistance(observation.OffsetXJupiterRadii, observation.OffsetYJupiterRadii, 1, context.earthMinorRadius),
effectiveRadius: satelliteRadius,
}, true
case JupiterGalileanEclipse:
xSunAU := vectorDot(stateVector, context.sunEast)
ySunAU := vectorDot(stateVector, context.sunNorth)
zSunAU := vectorDot(stateVector, context.sunLineOfSight)
umbraScale := jupiterUmbraScale(zSunAU, context.sunDistanceAU)
if zSunAU <= 0 || umbraScale <= 0 {
return jupiterGalileanContactGeometry{}, false
}
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
return jupiterGalileanContactGeometry{
signedDistance: ellipseSignedDistance(xSunAU/radiusAU, ySunAU/radiusAU, umbraScale, context.sunMinorRadius*umbraScale),
effectiveRadius: satelliteRadius,
}, true
case JupiterGalileanShadowTransit:
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
axisDenominator := vectorDot(context.sunLineOfSight, context.lineOfSight)
if math.Abs(axisDenominator) < 1e-12 {
return jupiterGalileanContactGeometry{}, false
}
axisDistanceAU := -vectorDot(stateVector, context.lineOfSight) / axisDenominator
if axisDistanceAU <= 0 {
return jupiterGalileanContactGeometry{}, false
}
axisPoint := Vector3{
stateVector[0] + axisDistanceAU*context.sunLineOfSight[0],
stateVector[1] + axisDistanceAU*context.sunLineOfSight[1],
stateVector[2] + axisDistanceAU*context.sunLineOfSight[2],
}
xAU := vectorDot(axisPoint, context.east)
yAU := vectorDot(axisPoint, context.north)
return jupiterGalileanContactGeometry{
signedDistance: ellipseSignedDistance(xAU/radiusAU, yAU/radiusAU, 1, context.earthMinorRadius),
effectiveRadius: jupiterGalileanPenumbraRadiusJupiterRadii(satellite, axisDistanceAU, context.sunDistanceAU),
}, true
default:
return jupiterGalileanContactGeometry{}, false
}
}
func jupiterGalileanEclipseSignedDistanceAt(jd float64, satellite int, penumbra bool) (float64, bool) {
if !isFinite(jd) || satellite < 1 || satellite > 4 {
return math.NaN(), false
}
evaluationJD := TD2UT(jd, true)
context := newJupiterGalileanObservationContext(evaluationJD)
if context.jupiterDistance == 0 {
return math.NaN(), false
}
state := context.observationForSatellite(satellite - 1).State
stateVector := Vector3{state.X, state.Y, state.Z}
xSunAU := vectorDot(stateVector, context.sunEast)
ySunAU := vectorDot(stateVector, context.sunNorth)
zSunAU := vectorDot(stateVector, context.sunLineOfSight)
if zSunAU <= 0 {
return math.NaN(), false
}
scale := jupiterUmbraScale(zSunAU, context.sunDistanceAU)
if penumbra {
scale = jupiterPenumbraScale(zSunAU, context.sunDistanceAU)
}
if scale <= 0 {
return math.NaN(), false
}
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
return ellipseSignedDistance(xSunAU/radiusAU, ySunAU/radiusAU, scale, context.sunMinorRadius*scale), true
}
func jupiterGalileanSatelliteRadiusJupiterRadii(satellite int) float64 {
switch satellite {
case 1:
return 1821.6 / jupiterGalileanEquatorialRadiusKM
case 2:
return 1560.8 / jupiterGalileanEquatorialRadiusKM
case 3:
return 2634.1 / jupiterGalileanEquatorialRadiusKM
case 4:
return 2410.3 / jupiterGalileanEquatorialRadiusKM
default:
return math.NaN()
}
}
func jupiterPenumbraScale(distanceBehindAU, sunDistanceAU float64) float64 {
if distanceBehindAU <= 0 || sunDistanceAU <= 0 {
return 0
}
jupiterRadiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
return 1 + distanceBehindAU*(solarRadiusAU+jupiterRadiusAU)/(sunDistanceAU*jupiterRadiusAU)
}
func jupiterGalileanUmbraRadiusJupiterRadii(satellite int, pathLengthAU, sunDistanceAU float64) float64 {
if pathLengthAU <= 0 || sunDistanceAU <= 0 {
return math.NaN()
}
satelliteRadiusAU := jupiterGalileanSatelliteRadiusJupiterRadii(satellite) * jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
umbraRadiusAU := satelliteRadiusAU - pathLengthAU*(solarRadiusAU-satelliteRadiusAU)/sunDistanceAU
if umbraRadiusAU <= 0 {
return math.NaN()
}
return umbraRadiusAU / (jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM)
}
func jupiterGalileanPenumbraRadiusJupiterRadii(satellite int, pathLengthAU, sunDistanceAU float64) float64 {
if pathLengthAU <= 0 || sunDistanceAU <= 0 {
return math.NaN()
}
satelliteRadiusAU := jupiterGalileanSatelliteRadiusJupiterRadii(satellite) * jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
penumbraRadiusAU := satelliteRadiusAU + pathLengthAU*(solarRadiusAU+satelliteRadiusAU)/sunDistanceAU
if penumbraRadiusAU <= 0 {
return math.NaN()
}
return penumbraRadiusAU / (jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM)
}
func jupiterGalileanShadowLimbMetricAt(jd float64, satellite int, penumbra bool) (float64, bool) {
if !isFinite(jd) || satellite < 1 || satellite > 4 {
return math.NaN(), false
}
evaluationJD := TD2UT(jd, true)
context := newJupiterGalileanObservationContext(evaluationJD)
if context.jupiterDistance == 0 {
return math.NaN(), false
}
state := context.observationForSatellite(satellite - 1).State
stateVector := Vector3{state.X, state.Y, state.Z}
satelliteBody := context.toBodyCoordinates(stateVector)
axisBody := normalizeVector(context.toBodyCoordinates(context.sunLineOfSight))
if vectorMagnitude(axisBody) == 0 {
return math.NaN(), false
}
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
satelliteRadiusAU := jupiterGalileanSatelliteRadiusJupiterRadii(satellite) * radiusAU
limbU, limbV, ok := jupiterGalileanVisibleLimbBasis(context)
if !ok {
return math.NaN(), false
}
metricAtAngle := func(angle float64) float64 {
limbPointBody := jupiterGalileanVisibleLimbPoint(angle, limbU, limbV, jupiterPolarRadiusRatio())
limbPointAU := Vector3{
limbPointBody[0] * radiusAU,
limbPointBody[1] * radiusAU,
limbPointBody[2] * radiusAU,
}
return jupiterGalileanShadowConeMetricForPoint(limbPointAU, satelliteBody, axisBody, satelliteRadiusAU, context.sunDistanceAU, penumbra)
}
return minimizeJupiterGalileanPeriodicMetric(metricAtAngle)
}
func jupiterGalileanVisibleLimbBasis(context jupiterGalileanObservationContext) (Vector3, Vector3, bool) {
polar := jupiterPolarRadiusRatio()
earthBody := context.toBodyCoordinates(context.earthDirection)
planeNormal := Vector3{earthBody[0], earthBody[1], earthBody[2] / polar}
planeNormal = normalizeVector(planeNormal)
if vectorMagnitude(planeNormal) == 0 {
return Vector3{}, Vector3{}, false
}
reference := Vector3{0, 0, 1}
if math.Abs(vectorDot(reference, planeNormal)) > 0.9 {
reference = Vector3{1, 0, 0}
}
u := normalizeVector(pxp(planeNormal, reference))
if vectorMagnitude(u) == 0 {
reference = Vector3{0, 1, 0}
u = normalizeVector(pxp(planeNormal, reference))
if vectorMagnitude(u) == 0 {
return Vector3{}, Vector3{}, false
}
}
v := normalizeVector(pxp(planeNormal, u))
if vectorMagnitude(v) == 0 {
return Vector3{}, Vector3{}, false
}
return u, v, true
}
func jupiterGalileanVisibleLimbPoint(angle float64, u, v Vector3, polar float64) Vector3 {
sinAngle := math.Sin(angle)
cosAngle := math.Cos(angle)
q := Vector3{
u[0]*cosAngle + v[0]*sinAngle,
u[1]*cosAngle + v[1]*sinAngle,
u[2]*cosAngle + v[2]*sinAngle,
}
return Vector3{q[0], q[1], polar * q[2]}
}
func jupiterGalileanShadowConeMetricForPoint(
pointAU, satelliteAU, axisUnit Vector3,
satelliteRadiusAU, sunDistanceAU float64,
penumbra bool,
) float64 {
vector := Vector3{
pointAU[0] - satelliteAU[0],
pointAU[1] - satelliteAU[1],
pointAU[2] - satelliteAU[2],
}
axisDistanceAU := vectorDot(vector, axisUnit)
if axisDistanceAU <= 0 {
return math.Inf(1)
}
perpendicular := Vector3{
vector[0] - axisDistanceAU*axisUnit[0],
vector[1] - axisDistanceAU*axisUnit[1],
vector[2] - axisDistanceAU*axisUnit[2],
}
perpendicularDistanceAU := vectorMagnitude(perpendicular)
if penumbra {
penumbraRadiusAU := satelliteRadiusAU + axisDistanceAU*(solarRadiusAU+satelliteRadiusAU)/sunDistanceAU
return perpendicularDistanceAU - penumbraRadiusAU
}
umbraRadiusAU := satelliteRadiusAU - axisDistanceAU*(solarRadiusAU-satelliteRadiusAU)/sunDistanceAU
return perpendicularDistanceAU - umbraRadiusAU
}
func minimizeJupiterGalileanPeriodicMetric(metric func(angle float64) float64) (float64, bool) {
const (
samples = 144
phi = 0.6180339887498948482
)
step := 2 * math.Pi / float64(samples)
bestAngle := 0.0
bestValue := math.Inf(1)
for i := 0; i < samples; i++ {
angle := float64(i) * step
value := metric(angle)
if value < bestValue {
bestValue = value
bestAngle = angle
}
}
if !isFinite(bestValue) {
return math.NaN(), false
}
left := bestAngle - step
right := bestAngle + step
x1 := right - phi*(right-left)
x2 := left + phi*(right-left)
f1 := metric(x1)
f2 := metric(x2)
for i := 0; i < 80; i++ {
if f1 <= f2 {
right = x2
x2 = x1
f2 = f1
x1 = right - phi*(right-left)
f1 = metric(x1)
} else {
left = x1
x1 = x2
f1 = f2
x2 = left + phi*(right-left)
f2 = metric(x2)
}
}
return math.Min(bestValue, metric((left+right)/2)), true
}
func ellipseSignedDistance(x, y, major, minor float64) float64 {
if major <= 0 || minor <= 0 {
return math.NaN()
}
targetX := math.Abs(x)
targetY := math.Abs(y)
if targetX == 0 && targetY == 0 {
if minor < major {
return -minor
}
return -major
}
left := 0.0
right := math.Pi / 2
const phi = 0.6180339887498948482
x1 := right - phi*(right-left)
x2 := left + phi*(right-left)
f1 := ellipseDistanceSquaredAtAngle(targetX, targetY, major, minor, x1)
f2 := ellipseDistanceSquaredAtAngle(targetX, targetY, major, minor, x2)
for i := 0; i < 80; i++ {
if f1 <= f2 {
right = x2
x2 = x1
f2 = f1
x1 = right - phi*(right-left)
f1 = ellipseDistanceSquaredAtAngle(targetX, targetY, major, minor, x1)
} else {
left = x1
x1 = x2
f1 = f2
x2 = left + phi*(right-left)
f2 = ellipseDistanceSquaredAtAngle(targetX, targetY, major, minor, x2)
}
}
distance := math.Sqrt(ellipseDistanceSquaredAtAngle(targetX, targetY, major, minor, (left+right)/2))
if ellipseInside(x, y, major, minor) {
return -distance
}
return distance
}
func ellipseDistanceSquaredAtAngle(x, y, major, minor, angle float64) float64 {
ellipseX := major * math.Cos(angle)
ellipseY := minor * math.Sin(angle)
dx := ellipseX - x
dy := ellipseY - y
return dx*dx + dy*dy
}
func invalidJupiterGalileanPhenomenonContactEvent() JupiterGalileanPhenomenonContactEvent {
return JupiterGalileanPhenomenonContactEvent{
Disappearance: JupiterGalileanPhenomenonContact{
Start: math.NaN(),
ModelCrossing: math.NaN(),
End: math.NaN(),
},
Greatest: math.NaN(),
Reappearance: JupiterGalileanPhenomenonContact{
Start: math.NaN(),
ModelCrossing: math.NaN(),
End: math.NaN(),
},
GreatestPhenomenon: invalidJupiterGalileanPhenomenon(),
}
}