Files
astro/basic/occultation_planet.go
T

524 lines
22 KiB
Go
Raw Normal View History

package basic
import (
"math"
"sort"
"time"
)
const (
planetOccultationDefaultStepDays = 0.25
planetOccultationCandidateLimitArcsec = 10 * 3600.0
planetOccultationLatitudeMarginArcsec = 3600.0
planetOccultationContactStepDays = 10.0 / 1440.0
planetOccultationContactSpanDays = 2.0
planetOccultationGrazingToleranceArcsec = 0.01
planetOccultationRootToleranceDays = occultationEventSelectionToleranceDays
planetOccultationMaxContactSteps = 10000
)
type planetOccultationConfig struct {
planet OccultationPlanet
planetIndex int
equatorialRadiusKM float64
apparentRaDecN func(float64, int) (float64, float64)
earthDistanceN func(float64, int) float64
semidiameterN func(float64, int) float64
}
type planetMoonPosition struct {
moonRA, moonDec float64
planetRA, planetDec float64
valid bool
}
type planetOccultationState struct {
position planetMoonPosition
separationArcsec float64
moonSemidiameter float64
planetSemidiameter float64
externalContactMetric float64
internalContactMetric float64
valid bool
}
func (state planetOccultationState) movingDiskContactState() movingDiskContactState {
return movingDiskContactState{
separation: state.separationArcsec,
occultingOuterRadius: state.moonSemidiameter,
occultingInnerRadius: state.moonSemidiameter,
targetRadius: state.planetSemidiameter,
valid: state.valid,
}
}
// FindPlanetOccultationsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func FindPlanetOccultationsWithInstance(start, end time.Time, planet OccultationPlanet, longitude, latitude, height float64,
options OccultationSearchOptions, inst *Instance) ([]PlanetOccultationInfo, error) {
if err := validateOccultationTimeRange(start, end); err != nil {
return nil, err
}
if err := planet.Validate(); err != nil {
return nil, err
}
observer := Observer{Longitude: longitude, Latitude: latitude, Height: height}
if err := observer.Validate(); err != nil {
return nil, err
}
if err := options.Validate(); err != nil {
return nil, err
}
config, _ := planetOccultationConfigFor(planet, inst)
startTT := occultationTimeToTT(start)
endTT := occultationTimeToTT(end)
resultLocation := start.Location()
candidates := planetOccultationCandidateGreatestTimes(
startTT, endTT, planetOccultationCoarseStepDays(options), config, &observer, options.SafetyMarginArcsec, inst,
)
results := make([]PlanetOccultationInfo, 0, len(candidates))
for _, greatestTT := range candidates {
info, ok := planetOccultationInfoWithLimb(greatestTT, config, observer, options.SafetyMarginArcsec, resultLocation, options.LunarLimb, inst)
if !ok {
continue
}
if len(results) == 0 || math.Abs(results[len(results)-1].Greatest.Sub(info.Greatest).Seconds()) > 60 {
results = append(results, info)
if options.MaxEvents > 0 && len(results) >= options.MaxEvents {
break
}
}
}
sort.SliceStable(results, func(i, j int) bool { return results[i].Greatest.Before(results[j].Greatest) })
return results, nil
}
// FindBestPlanetOccultationsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func FindBestPlanetOccultationsWithInstance(start, end time.Time, planet OccultationPlanet,
options OccultationSearchOptions, inst *Instance) ([]PlanetOccultationInfo, error) {
if err := validateOccultationTimeRange(start, end); err != nil {
return nil, err
}
if err := planet.Validate(); err != nil {
return nil, err
}
if err := options.Validate(); err != nil {
return nil, err
}
config, _ := planetOccultationConfigFor(planet, inst)
startTT := occultationTimeToTT(start)
endTT := occultationTimeToTT(end)
selectionStartTT := startTT - occultationEventSelectionToleranceDays
selectionEndTT := endTT + occultationEventSelectionToleranceDays
candidateStartTT := startTT - occultationPathSearchSpanDays
candidateEndTT := endTT + occultationPathSearchSpanDays
resultLocation := start.Location()
candidates := planetOccultationCandidateGreatestTimes(
candidateStartTT, candidateEndTT, planetOccultationCoarseStepDays(options), config, nil, options.SafetyMarginArcsec, inst,
)
results := make([]PlanetOccultationInfo, 0, len(candidates))
for _, seedTT := range candidates {
greatestTT, observer, _, observerOK := planetOccultationBestObserver(seedTT, selectionStartTT, selectionEndTT, config, inst)
if !observerOK {
continue
}
info, ok := planetOccultationInfoWithLimb(greatestTT, config, observer, options.SafetyMarginArcsec, resultLocation, options.LunarLimb, inst)
if !ok {
continue
}
if len(results) == 0 || math.Abs(results[len(results)-1].Greatest.Sub(info.Greatest).Seconds()) > 60 {
results = append(results, info)
if options.MaxEvents > 0 && len(results) >= options.MaxEvents {
break
}
}
}
sort.SliceStable(results, func(i, j int) bool { return results[i].Greatest.Before(results[j].Greatest) })
return results, nil
}
func planetOccultationConfigFor(planet OccultationPlanet, instances ...*Instance) (planetOccultationConfig, bool) {
inst := instanceValue(instances)
config := planetOccultationConfig{planet: planet}
switch planet {
case OccultationMercury:
config.planetIndex = 1
config.equatorialRadiusKM = mercuryEquatorialRadiusKM
config.apparentRaDecN = func(jde float64, n int) (float64, float64) { return MercuryApparentRaDecNWithInstance(jde, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthMercuryAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return MercurySemidiameterNWithInstance(jde, n, inst) }
case OccultationVenus:
config.planetIndex = 2
config.equatorialRadiusKM = venusEquatorialRadiusKM
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return VenusApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthVenusAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return VenusSemidiameterNWithInstance(jde, n, inst) }
case OccultationMars:
config.planetIndex = 3
config.equatorialRadiusKM = marsEquatorialRadiusKM
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return MarsApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthMarsAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return MarsSemidiameterNWithInstance(jde, n, inst) }
case OccultationJupiter:
config.planetIndex = 4
config.equatorialRadiusKM = jupiterEquatorialRadiusKM
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return JupiterApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthJupiterAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return JupiterSemidiameterNWithInstance(jde, n, inst) }
case OccultationSaturn:
config.planetIndex = 5
config.equatorialRadiusKM = saturnEquatorialRadiusKM
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return SaturnApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthSaturnAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return SaturnSemidiameterNWithInstance(jde, n, inst) }
case OccultationUranus:
config.planetIndex = 6
config.equatorialRadiusKM = uranusEquatorialRadiusKM
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return UranusApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthUranusAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return UranusSemidiameterNWithInstance(jde, n, inst) }
case OccultationNeptune:
config.planetIndex = 7
config.equatorialRadiusKM = neptuneEquatorialRadiusKM
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return NeptuneApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthNeptuneAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return NeptuneSemidiameterNWithInstance(jde, n, inst) }
default:
return planetOccultationConfig{}, false
}
return config, true
}
func planetOccultationCandidateGreatestTimes(
startTT, endTT, step float64,
config planetOccultationConfig,
observer *Observer,
safetyMarginArcsec float64, instances ...*Instance) []float64 {
inst := instanceValue(instances)
if endTT <= startTT {
return nil
}
duration := endTT - startTT
if step > duration/4 {
step = math.Max(duration/4, 0.25/86400.0)
}
step = math.Max(step, 0.25/86400.0)
scanStart := startTT - step
scanEnd := endTT + step
leftTT := scanStart
centerTT := math.Min(leftTT+step, scanEnd)
leftValue := planetOccultationExternalContactMetric(leftTT, config, observer, 8, inst)
centerValue := planetOccultationExternalContactMetric(centerTT, config, observer, 8, inst)
results := make([]float64, 0)
for centerTT < scanEnd {
rightTT := math.Min(centerTT+step, scanEnd)
rightValue := planetOccultationExternalContactMetric(rightTT, config, observer, 8, inst)
candidateLimit := planetOccultationCandidateLimitArcsec + safetyMarginArcsec
if finite(leftValue) && finite(centerValue) && finite(rightValue) &&
centerValue <= leftValue && centerValue <= rightValue && centerValue <= candidateLimit {
// 最小外接触度量同时包含动态月面和行星盘面,因此定义事件是否存在以及报告的掩甚时刻。
// The minimum outer-contact metric includes both dynamic disks and therefore defines event existence and the reported greatest instant.
greatestTT := planetOccultationMinimizeExternalMetric(leftTT, rightTT, config, observer, inst)
if greatestTT >= startTT && greatestTT <= endTT &&
planetOccultationLatitudePass(greatestTT, config, observer, safetyMarginArcsec, inst) {
if len(results) == 0 || math.Abs(greatestTT-results[len(results)-1]) > 60.0/86400.0 {
results = append(results, greatestTT)
}
}
}
leftTT, leftValue = centerTT, centerValue
centerTT, centerValue = rightTT, rightValue
}
return results
}
func planetOccultationMinimizeExternalMetric(left, right float64, config planetOccultationConfig, observer *Observer, instances ...*Instance) float64 {
inst := instanceValue(instances)
if right <= left {
return left
}
const goldenRatio = 0.6180339887498949
x1 := right - goldenRatio*(right-left)
x2 := left + goldenRatio*(right-left)
f1 := planetOccultationExternalContactMetric(x1, config, observer, -1, inst)
f2 := planetOccultationExternalContactMetric(x2, config, observer, -1, inst)
for i := 0; i < 64 && right-left > planetOccultationRootToleranceDays; i++ {
if f1 > f2 {
left = x1
x1, f1 = x2, f2
x2 = left + goldenRatio*(right-left)
f2 = planetOccultationExternalContactMetric(x2, config, observer, -1, inst)
} else {
right = x2
x2, f2 = x1, f1
x1 = right - goldenRatio*(right-left)
f1 = planetOccultationExternalContactMetric(x1, config, observer, -1, inst)
}
}
return (left + right) / 2
}
func planetOccultationBestObserver(seedTT, startTT, endTT float64, config planetOccultationConfig, instances ...*Instance) (float64, Observer, float64, bool) {
inst := instanceValue(instances)
frameAt := func(tt float64) (occultationPathFrame, bool) {
return planetOccultationPathFrameAt(tt, config, inst)
}
searchStart := seedTT - occultationPathSearchSpanDays
searchEnd := seedTT + occultationPathSearchSpanDays
outerStart, outerEnd, ok := occultationPathWindowForFrame(seedTT, searchStart, searchEnd, frameAt, false)
if !ok {
return 0, Observer{}, 0, false
}
greatestTT := occultationPathGreatestForFrame(seedTT, outerStart, outerEnd, frameAt)
if greatestTT < startTT || greatestTT > endTT {
return 0, Observer{}, 0, false
}
point, pointOK := occultationPathCenterPointForFrame(greatestTT, frameAt, time.UTC)
if !pointOK {
// 非中心事件的月影轴不与地球椭球相交,最佳站心应取离影轴最近的椭球点;
// 外接触切点位于掩带边缘,会把掩甚站心推出掩食之外,只作最后回退。
// For a non-central event the shadow axis misses the ellipsoid, so the
// best station is the ellipsoid point nearest to that axis. The outer
// contact tangent sits on the band edge and moves the greatest station
// outside the occultation, so it remains the last resort only.
point, pointOK = occultationPathTrackPointForFrame(greatestTT, frameAt, time.UTC)
if !pointOK {
point, pointOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, time.UTC, inst)
}
}
if !pointOK {
return 0, Observer{}, 0, false
}
observer := Observer{Longitude: point.Longitude, Latitude: point.Latitude}
state := planetOccultationStateAt(greatestTT, config, &observer, -1, inst)
if !state.valid {
return 0, Observer{}, 0, false
}
return greatestTT, observer, state.externalContactMetric, true
}
func planetOccultationInfoAtGreatest(
greatestTT float64,
config planetOccultationConfig,
observer Observer,
safetyMarginArcsec float64,
location *time.Location, instances ...*Instance) (PlanetOccultationInfo, bool) {
inst := instanceValue(instances)
info, state, ok := planetOccultationGreatestInfo(greatestTT, config, observer, safetyMarginArcsec, location, inst)
if !ok || state.externalContactMetric > 0 {
return PlanetOccultationInfo{}, false
}
if math.Abs(state.externalContactMetric) <= planetOccultationGrazingToleranceArcsec {
info.Type = OccultationGrazing
info.ExternalImmersion = info.Greatest
info.ExternalEmersion = info.Greatest
info.ContactsComplete = true
return info, true
}
return planetOccultationCircularContacts(info, state, greatestTT, config, observer, location, inst)
}
// planetOccultationGreatestInfo 只填掩甚几何,不判断圆月面是否成掩、不求接触。
func planetOccultationGreatestInfo(greatestTT float64, config planetOccultationConfig, observer Observer, safetyMarginArcsec float64, location *time.Location, inst *Instance) (PlanetOccultationInfo, planetOccultationState, bool) {
if !planetOccultationLatitudePass(greatestTT, config, &observer, safetyMarginArcsec, inst) {
return PlanetOccultationInfo{}, planetOccultationState{}, false
}
state := planetOccultationStateAt(greatestTT, config, &observer, -1, inst)
if !state.valid {
return PlanetOccultationInfo{}, planetOccultationState{}, false
}
info := PlanetOccultationInfo{
Planet: config.planet,
TargetID: config.planet.String(),
Observer: observer,
Type: OccultationPartial,
Greatest: occultationTTToLocation(greatestTT, location),
MinimumSeparationArcsec: state.separationArcsec,
PositionAngleDeg: occultationPositionAngle(state.position.moonRA, state.position.moonDec, state.position.planetRA, state.position.planetDec),
MoonSemidiameterArcsec: state.moonSemidiameter,
PlanetSemidiameterArcsec: state.planetSemidiameter,
MoonAltitudeAtGreatest: occultationAltitude(greatestTT, observer, state.position.moonRA, state.position.moonDec),
MoonAzimuthAtGreatest: occultationAzimuth(greatestTT, observer, state.position.moonRA, state.position.moonDec),
}
info.VisibleAtGreatest = info.MoonAltitudeAtGreatest >= 0
return info, state, true
}
func planetOccultationCircularContacts(info PlanetOccultationInfo, state planetOccultationState, greatestTT float64, config planetOccultationConfig, observer Observer, location *time.Location, inst *Instance) (PlanetOccultationInfo, bool) {
contactEvaluator := newPlanetOccultationContactEvaluator(config, observer, inst)
contactEvaluator.prime(greatestTT, state.movingDiskContactState(), state.valid)
externalImmersionTT, externalImmersionOK := planetOccultationContactWithEvaluator(greatestTT, -1, false, contactEvaluator)
externalEmersionTT, externalEmersionOK := planetOccultationContactWithEvaluator(greatestTT, 1, false, contactEvaluator)
if !externalImmersionOK || !externalEmersionOK || externalEmersionTT <= externalImmersionTT {
return PlanetOccultationInfo{}, false
}
info.ExternalImmersion = occultationTTToLocation(externalImmersionTT, location)
info.ExternalEmersion = occultationTTToLocation(externalEmersionTT, location)
if state.internalContactMetric < -planetOccultationGrazingToleranceArcsec {
internalImmersionTT, internalImmersionOK := planetOccultationContactWithEvaluator(greatestTT, -1, true, contactEvaluator)
internalEmersionTT, internalEmersionOK := planetOccultationContactWithEvaluator(greatestTT, 1, true, contactEvaluator)
if !internalImmersionOK || !internalEmersionOK ||
internalImmersionTT <= externalImmersionTT || internalEmersionTT >= externalEmersionTT ||
internalImmersionTT >= greatestTT || internalEmersionTT <= greatestTT {
return PlanetOccultationInfo{}, false
}
info.Type = OccultationTotal
info.InternalImmersion = occultationTTToLocation(internalImmersionTT, location)
info.InternalEmersion = occultationTTToLocation(internalEmersionTT, location)
info.HasInternalContacts = true
}
info.ContactsComplete = true
return info, true
}
func planetMoonPositionAt(tt float64, config planetOccultationConfig, observer *Observer, n int, instances ...*Instance) planetMoonPosition {
inst := instanceValue(instances)
moonRA, moonDec := HMoonGeocentricApparentRaDecNWithInstance(tt, n, inst)
planetRA, planetDec := config.apparentRaDecN(tt, n)
if observer != nil {
ut := TT2UTC(tt)
moonDistanceAU := HMoonAwayNWithInstance(tt, n, inst) / angularDiameterAstronomicalUnitKM
planetDistanceAU := config.earthDistanceN(tt, n)
moonRA, moonDec = TopocentricRaDec(moonRA, moonDec, observer.Latitude, observer.Longitude, ut, moonDistanceAU, observer.Height)
planetRA, planetDec = TopocentricRaDec(planetRA, planetDec, observer.Latitude, observer.Longitude, ut, planetDistanceAU, observer.Height)
moonRA = normalizeRA(moonRA)
planetRA = normalizeRA(planetRA)
}
return planetMoonPosition{
moonRA: moonRA,
moonDec: moonDec,
planetRA: planetRA,
planetDec: planetDec,
valid: finite(moonRA) && finite(moonDec) && finite(planetRA) && finite(planetDec),
}
}
func planetOccultationStateAt(tt float64, config planetOccultationConfig, observer *Observer, n int, instances ...*Instance) planetOccultationState {
inst := instanceValue(instances)
position := planetMoonPositionAt(tt, config, observer, n, inst)
moonRadius := MoonSemidiameterNWithInstance(tt, n, inst)
planetRadius := config.semidiameterN(tt, n)
if observer != nil {
moonRadius = moonTopocentricSemidiameterN(tt, *observer, n, inst)
planetRadius = planetTopocentricSemidiameterN(tt, config, *observer, n)
}
if !position.valid || !finite(moonRadius) || !finite(planetRadius) || moonRadius <= planetRadius || planetRadius <= 0 {
return planetOccultationState{}
}
separation := angularSeparationDegrees(position.moonRA, position.moonDec, position.planetRA, position.planetDec) * 3600
contactState := movingDiskContactState{
separation: separation,
occultingOuterRadius: moonRadius,
occultingInnerRadius: moonRadius,
targetRadius: planetRadius,
valid: movingDiskContactStateValid(
separation, moonRadius, moonRadius, planetRadius,
),
}
return planetOccultationState{
position: position,
separationArcsec: separation,
moonSemidiameter: moonRadius,
planetSemidiameter: planetRadius,
externalContactMetric: contactState.externalContactGap(),
internalContactMetric: contactState.internalContactGap(),
valid: contactState.valid,
}
}
func planetTopocentricSemidiameterN(tt float64, config planetOccultationConfig, observer Observer, n int) float64 {
ra, dec := config.apparentRaDecN(tt, n)
distanceKM := config.earthDistanceN(tt, n) * angularDiameterAstronomicalUnitKM
if !finite(ra) || !finite(dec) || !finite(distanceKM) || distanceKM <= 0 {
return math.NaN()
}
distanceKM = topocentricDistanceKM(ra, dec, distanceKM, observer, TT2UTC(tt))
if !finite(distanceKM) || distanceKM <= config.equatorialRadiusKM {
return math.NaN()
}
return angularSemidiameterArcsec(config.equatorialRadiusKM, distanceKM)
}
func planetOccultationExternalContactMetric(tt float64, config planetOccultationConfig, observer *Observer, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
state := planetOccultationStateAt(tt, config, observer, n, inst)
if !state.valid {
return math.Inf(1)
}
return state.externalContactMetric
}
func planetOccultationLatitudePass(tt float64, config planetOccultationConfig, observer *Observer, safetyMarginArcsec float64, instances ...*Instance) bool {
inst := instanceValue(instances)
state := planetOccultationStateAt(tt, config, observer, -1, inst)
if !state.valid {
return false
}
_, moonLatitude := RaDecToLoBo(tt, state.position.moonRA, state.position.moonDec)
_, planetLatitude := RaDecToLoBo(tt, state.position.planetRA, state.position.planetDec)
limit := state.moonSemidiameter + state.planetSemidiameter + planetOccultationLatitudeMarginArcsec + safetyMarginArcsec
return math.Abs(moonLatitude-planetLatitude)*3600 <= limit
}
func newPlanetOccultationContactEvaluator(
config planetOccultationConfig,
observer Observer, instances ...*Instance) *movingDiskContactEvaluator {
inst := instanceValue(instances)
return newMovingDiskContactEvaluator(func(tt float64) (movingDiskContactState, bool) {
state := planetOccultationStateAt(tt, config, &observer, -1, inst)
return state.movingDiskContactState(), state.valid
})
}
func planetOccultationContactWithEvaluator(
greatestTT float64,
direction int,
internal bool,
evaluator *movingDiskContactEvaluator,
) (float64, bool) {
if direction != -1 && direction != 1 {
return math.NaN(), false
}
metric := func(tt float64) (float64, bool) {
return evaluator.gap(tt, internal)
}
root, ok := occultationMovingDiskEngine().contactRoot(
greatestTT,
float64(direction),
planetOccultationContactStepDays,
planetOccultationContactSpanDays,
planetOccultationRootToleranceDays,
metric,
64,
)
if !ok {
return math.NaN(), false
}
return root, true
}
func planetOccultationCoarseStepDays(options OccultationSearchOptions) float64 {
step := planetOccultationDefaultStepDays
if options.MaxStep > 0 {
requested := options.MaxStep.Hours() / 24
if requested > 0 && requested < step {
step = requested
}
}
return math.Max(step, occultationSearchMinimumStep.Hours()/24)
}