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