Files
astro/basic/rise_set_altitude.go
T

299 lines
12 KiB
Go
Raw Normal View History

package basic
import (
"math"
"time"
)
// AltitudeLimb 日月取高度的位置;行星与恒星忽略 / limb used for Sun and Moon altitude.
type AltitudeLimb int
const (
// AltitudeLimbUpper 上缘,与日出日落口径相同 / upper limb.
AltitudeLimbUpper AltitudeLimb = iota
// AltitudeLimbCenter 中心 / centre.
AltitudeLimbCenter
// AltitudeLimbLower 下缘 / lower limb.
AltitudeLimbLower
)
// AltitudeCrossingOptions 任意高度升落选项;零值为上缘、标准气象(1010 hPa、10 °C)、计入折射 / options for altitude crossings.
type AltitudeCrossingOptions struct {
Limb AltitudeLimb
// NoRefraction 为 true 时目标高度按几何高度比较 / compare geometric altitude when true.
NoRefraction bool
// PressureHPa 为 0 时用标准气压与气温 / zero selects the standard atmosphere.
PressureHPa float64
TemperatureC float64
}
// AltitudeCrossingState 一个民用日内的穿越状态 / crossing state over one civil day.
type AltitudeCrossingState int
const (
// AltitudeCrosses 至少有一次升或落 / at least one crossing.
AltitudeCrosses AltitudeCrossingState = iota
// AltitudeAlwaysAbove 全天高于目标高度 / always above the target altitude.
AltitudeAlwaysAbove
// AltitudeAlwaysBelow 全天低于目标高度 / always below the target altitude.
AltitudeAlwaysBelow
// AltitudeTangent 极值与目标高度相差不足 1″,视为相切 / extremum within 1″ of the target.
AltitudeTangent
)
// AltitudeCrossing 民用日内首次升、落(本地儒略日,无则为 NaN)与残差范围(度)/ first rise and set in local JD with residual range in degrees.
type AltitudeCrossing struct {
Rise, Set float64
State AltitudeCrossingState
MinResidual float64
MaxResidual float64
}
// AltitudeCrossingResult date 所在民用日内首次升、落(无则为零值,对应 HasRise/HasSet 为 false)与残差范围(度)/ first rise and set on the civil day.
type AltitudeCrossingResult struct {
Rise, Set time.Time
HasRise, HasSet bool
State AltitudeCrossingState
MinResidual float64
MaxResidual float64
}
const (
altitudeScanStepDays = 10.0 / 1440
altitudeTangentDegrees = 1.0 / 3600
)
func (options AltitudeCrossingOptions) refraction(trueAltitude float64) float64 {
if options.NoRefraction {
return 0
}
if options.PressureHPa == 0 {
return RefractionFromTrueAltitude(trueAltitude, refractionStandardPressureHPa, refractionStandardTemperatureC)
}
return RefractionFromTrueAltitude(trueAltitude, options.PressureHPa, options.TemperatureC)
}
func (options AltitudeCrossingOptions) limbOffset(semidiameterDeg float64) float64 {
switch options.Limb {
case AltitudeLimbUpper:
return semidiameterDeg
case AltitudeLimbLower:
return -semidiameterDeg
}
return 0
}
func (options AltitudeCrossingOptions) valid() bool {
if options.Limb < AltitudeLimbUpper || options.Limb > AltitudeLimbLower {
return false
}
return options.NoRefraction || options.PressureHPa == 0 || !math.IsNaN(RefractionFromTrueAltitude(0, options.PressureHPa, options.TemperatureC))
}
// SkyAltitudeCrossingWithInstance 日、月或行星在本地民用日 julianDay 内穿过视高度 targetAltitude(度,相对天文地平)的时刻 /
// Sun, Moon or planet crossings of an apparent altitude on the local civil day.
//
// height 只影响月球站心视差;需要以下沉地平为基准时由调用方从 targetAltitude 中减去地平俯角。
//
// 这是按儒略日窗口求解的低级接口:julianDay 与 timeZone(小时)由调用方拼出本地民用日,
// 固定时区不处理夏令时切换。julianDay 须取本地零点(x.5),正午及之后的值会落到次日窗口。一般用途请用 SkyAltitudeCrossingOnDate,它从 time.Time 推出
// 当天的真实时区窗口 / low-level julian-day window API; prefer SkyAltitudeCrossingOnDate.
func SkyAltitudeCrossingWithInstance(body SkyBody, julianDay, longitude, latitude, timeZone, height, targetAltitude float64,
options AltitudeCrossingOptions, inst *Instance) (AltitudeCrossing, error) {
altitude, err := skyAltitudeFunc(body, longitude, latitude, timeZone, height, inst)
if err != nil {
return AltitudeCrossing{}, err
}
start := math.Floor(julianDay) + 0.5
return altitudeCrossing(start, start+1, latitude, height, targetAltitude, options, altitude)
}
// SkyAltitudeCrossingOnDate 日、月或行星在 date 所在民用日内穿过视高度 targetAltitude 的时刻;夏令时切换日按实际日长搜索 /
// Sun, Moon or planet crossings of an apparent altitude on the civil day of date.
func SkyAltitudeCrossingOnDate(body SkyBody, date time.Time, longitude, latitude, height, targetAltitude float64,
options AltitudeCrossingOptions, inst *Instance) (AltitudeCrossingResult, error) {
start, end, timeZone := civilDayWindow(date)
altitude, err := skyAltitudeFunc(body, longitude, latitude, timeZone, height, inst)
if err != nil {
return AltitudeCrossingResult{}, err
}
raw, err := altitudeCrossing(start, end, latitude, height, targetAltitude, options, altitude)
return altitudeCrossingOnDate(date, timeZone, raw, err)
}
// StarAltitudeCrossing 恒星(视赤经、视赤纬,度)在本地民用日内穿过视高度 targetAltitude 的时刻 / star crossings of an apparent altitude.
//
// julianDay 须取本地零点(x.5),正午及之后的值会落到次日窗口;一般用途请用 StarAltitudeCrossingOnDate。
func StarAltitudeCrossing(julianDay, ra, dec, longitude, latitude, timeZone, targetAltitude float64, options AltitudeCrossingOptions) (AltitudeCrossing, error) {
if !isFiniteFloat(ra) || !isFiniteFloat(dec) {
return AltitudeCrossing{}, ErrInvalidObservationInput
}
start := math.Floor(julianDay) + 0.5
return altitudeCrossing(start, start+1, latitude, 0, targetAltitude, options, starAltitudeFunc(ra, dec, longitude, latitude, timeZone))
}
// StarAltitudeCrossingOnDate 恒星在 date 所在民用日内穿过视高度 targetAltitude 的时刻;夏令时切换日按实际日长搜索 /
// star crossings of an apparent altitude on the civil day of date.
func StarAltitudeCrossingOnDate(date time.Time, ra, dec, longitude, latitude, targetAltitude float64, options AltitudeCrossingOptions) (AltitudeCrossingResult, error) {
if !isFiniteFloat(ra) || !isFiniteFloat(dec) {
return AltitudeCrossingResult{}, ErrInvalidObservationInput
}
start, end, timeZone := civilDayWindow(date)
raw, err := altitudeCrossing(start, end, latitude, 0, targetAltitude, options, starAltitudeFunc(ra, dec, longitude, latitude, timeZone))
return altitudeCrossingOnDate(date, timeZone, raw, err)
}
// civilDayWindow 以当天零点的偏移固定本地时标,窗口为真实日长(夏令时切换日 23 或 25 小时)。
func civilDayWindow(date time.Time) (start, end, timeZone float64) {
year, month, day := date.Date()
midnight := time.Date(year, month, day, 0, 0, 0, 0, date.Location())
next := time.Date(year, month, day+1, 0, 0, 0, 0, date.Location())
_, offset := midnight.Zone()
start = Date2JD(midnight)
return start, start + next.Sub(midnight).Hours()/24, float64(offset) / 3600
}
func altitudeCrossingOnDate(date time.Time, timeZone float64, raw AltitudeCrossing, err error) (AltitudeCrossingResult, error) {
if err != nil {
return AltitudeCrossingResult{}, err
}
convert := func(jd float64) (time.Time, bool) {
if math.IsNaN(jd) {
return time.Time{}, false
}
return JD2DateByZone(jd-timeZone/24, date.Location(), false), true
}
out := AltitudeCrossingResult{State: raw.State, MinResidual: raw.MinResidual, MaxResidual: raw.MaxResidual}
out.Rise, out.HasRise = convert(raw.Rise)
out.Set, out.HasSet = convert(raw.Set)
return out, nil
}
func skyAltitudeFunc(body SkyBody, longitude, latitude, timeZone, height float64, inst *Instance) (func(jd float64) (float64, float64), error) {
switch {
case body == SkySun:
return func(jd float64) (float64, float64) {
state := sunObservationStateN(jd, longitude, latitude, timeZone, -1, inst)
return state.altitude, angularSemidiameterFromAU(sunEquatorialRadiusKM, state.distanceAU) / 3600
}, nil
case body == SkyMoon:
return func(jd float64) (float64, float64) {
state := hMoonObservationStateN(jd, longitude, latitude, timeZone, height, -1, inst)
return state.altitude, angularSemidiameterArcsec(moonEquatorialRadiusKM, state.distanceKM) / 3600
}, nil
case body.valid():
heightFn := skyPlanetHeight(body)
return func(jd float64) (float64, float64) { return heightFn(jd, longitude, latitude, timeZone, inst), 0 }, nil
}
return nil, ErrInvalidObservationInput
}
func starAltitudeFunc(ra, dec, longitude, latitude, timeZone float64) func(jd float64) (float64, float64) {
return func(jd float64) (float64, float64) {
return StarHeight(jd, ra, dec, longitude, latitude, timeZone), 0
}
}
func skyPlanetHeight(body SkyBody) func(localJD, lon, lat, timezone float64, inst *Instance) float64 {
switch body {
case SkyMercury:
return MercuryHeightWithInstance
case SkyVenus:
return VenusHeightWithInstance
case SkyMars:
return MarsHeightWithInstance
case SkyJupiter:
return JupiterHeightWithInstance
case SkySaturn:
return SaturnHeightWithInstance
case SkyUranus:
return UranusHeightWithInstance
}
return NeptuneHeightWithInstance
}
func altitudeCrossing(start, end, latitude, height, targetAltitude float64, options AltitudeCrossingOptions,
altitude func(jd float64) (float64, float64)) (AltitudeCrossing, error) {
if !isFiniteFloat(start) || !isFiniteFloat(end) || !(end > start) || !isFiniteFloat(latitude) || math.Abs(latitude) > 90 ||
!isFiniteFloat(height) || !isFiniteFloat(targetAltitude) || math.Abs(targetAltitude) > 90 || !options.valid() {
return AltitudeCrossing{}, ErrInvalidObservationInput
}
residual := func(jd float64) float64 {
alt, semidiameter := altitude(jd)
return alt + options.refraction(alt) + options.limbOffset(semidiameter) - targetAltitude
}
result := AltitudeCrossing{Rise: math.NaN(), Set: math.NaN(), MinResidual: math.Inf(1), MaxResidual: math.Inf(-1)}
prevT, prevR := start, residual(start)
if math.IsNaN(prevR) {
return AltitudeCrossing{}, ErrInvalidObservationInput
}
track := func(t, r float64) {
result.MinResidual = math.Min(result.MinResidual, r)
result.MaxResidual = math.Max(result.MaxResidual, r)
}
track(prevT, prevR)
record := func(root float64, rising bool) {
if rising && math.IsNaN(result.Rise) {
result.Rise = root
} else if !rising && math.IsNaN(result.Set) {
result.Set = root
}
}
// 同号区间内朝目标一侧的极值可能越过目标高度,采样点看不到这两次穿越。
refine := func(a, b, ra float64) {
peak, rp := orbitalGoldenExtremum(residual, a, b, ra < 0)
if math.IsNaN(rp) {
return
}
track(peak, rp)
if (rp < 0) == (ra < 0) || peak <= a || peak >= b {
return
}
record(orbitalBisect(residual, a, peak, ra), ra < 0)
record(orbitalBisect(residual, peak, b, rp), ra >= 0)
}
prev2T, prev2R, samples := prevT, prevR, 1
for t := start + altitudeScanStepDays; prevT < end; t += altitudeScanStepDays {
t = math.Min(t, end)
r := residual(t)
if math.IsNaN(r) {
return AltitudeCrossing{}, ErrInvalidObservationInput
}
track(t, r)
sameSign := (prevR < 0) == (r < 0)
switch {
case !sameSign:
record(orbitalBisect(residual, prevT, t, prevR), r >= 0)
case samples == 1:
refine(prevT, t, prevR)
case (prev2R < 0) == (prevR < 0) && altitudeTowardTarget(prev2R, prevR, r):
refine(prev2T, t, prev2R)
}
prev2T, prev2R = prevT, prevR
prevT, prevR = t, r
samples++
}
if samples > 2 && (prev2R < 0) == (prevR < 0) {
refine(prev2T, prevT, prev2R)
}
switch {
case !math.IsNaN(result.Rise) || !math.IsNaN(result.Set):
result.State = AltitudeCrosses
case math.Abs(result.MinResidual) < altitudeTangentDegrees || math.Abs(result.MaxResidual) < altitudeTangentDegrees:
result.State = AltitudeTangent
case result.MinResidual > 0:
result.State = AltitudeAlwaysAbove
default:
result.State = AltitudeAlwaysBelow
}
return result, nil
}
// altitudeTowardTarget 报告中间样本是否为朝零点一侧的局部极值:负残差取极大,正残差取极小。
func altitudeTowardTarget(left, middle, right float64) bool {
if middle < 0 {
return middle >= left && middle >= right
}
return middle <= left && middle <= right
}