4 Commits

Author SHA1 Message Date
b612 34ff6a36ae fix: 修正行星事件边界与留点计算
- 统一 UT 事件时刻与 TT 查询时刻的边界判断
- 将外行星留点搜索锚定到对应冲日周期
- 修正水星、金星合日、留、大距事件选择
- 统一七大行星视位置计算辅助逻辑
- 增加公开 Last/Next 边界和 JPL/NAOJ 基线回归测试
2026-05-22 12:24:41 +08:00
b612 d40c4dfcd9 fix: 兼容 TinyGo wasm 编译行星星历初始化
- 将 planetViews 从包级初始化改为 sync.Once 懒加载,避免 TinyGo interp 在编译期处理大型 float64 表切片索引时触发 unsupported fcmp
- 将行星视图构建失败改为内部返回 error,并由兼容层统一 panic
- 补充无效行星数据切片边界测试
2026-05-17 21:19:23 +08:00
b612 bec7b8a0d8 feat: 增强日月食搜索、沙罗周期与内行星凌日
- 使用压缩表加速查找日月食沙罗周期信息
- 优化日月食搜索跳步,减少非食季朔望月扫描
- 新增本地日全食、日环食、月全食搜索接口,返回 ok 区分未找到结果
- 新增水星、金星地心凌日查询及测试
2026-05-03 19:00:08 +08:00
b612 3ffdbe0034 feat: 扩展天文计算能力
- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息
- 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算
- 新增木星伽利略卫星位置、现象与接触事件计算
- 新增恒星星表、星座判定、自行修正与观测辅助能力
- 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包
- 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算
- 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试
- 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试
- 更新中文和英文 README,补充示例、精度说明、SVG 配图
2026-05-01 22:38:44 +08:00
391 changed files with 87921 additions and 17703 deletions
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+1842
View File
File diff suppressed because it is too large Load Diff
+1923
View File
File diff suppressed because it is too large Load Diff
+123
View File
@@ -0,0 +1,123 @@
package astro_test
import (
"encoding/json"
"math"
"os"
"testing"
"time"
"b612.me/astro/basic"
"b612.me/astro/moon"
"b612.me/astro/planet"
"b612.me/astro/sun"
)
type baselinePlanetSnapshot struct {
Name string `json:"name"`
XT int `json:"xt"`
LonBits uint64 `json:"lon_bits"`
LatBits uint64 `json:"lat_bits"`
RadBits uint64 `json:"rad_bits"`
}
type baselineMoonSnapshot struct {
LonBits uint64 `json:"lon_bits"`
LatBits uint64 `json:"lat_bits"`
DisBits uint64 `json:"dis_bits"`
}
type baselineSample struct {
UTC string `json:"utc"`
TTJD float64 `json:"tt_jd"`
Planets []baselinePlanetSnapshot `json:"planets"`
Moon baselineMoonSnapshot `json:"moon"`
}
func loadBaselineSamples(t *testing.T) []baselineSample {
t.Helper()
data, err := os.ReadFile("testdata/planet_moon_baseline.json")
if err != nil {
t.Fatal(err)
}
var samples []baselineSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatal(err)
}
if len(samples) == 0 {
t.Fatal("empty baseline samples")
}
return samples
}
func TestPlanetMoonBaselineRegression(t *testing.T) {
samples := loadBaselineSamples(t)
for _, sample := range samples {
for _, body := range sample.Planets {
gotLon := planet.WherePlanet(body.XT, 0, sample.TTJD)
if math.Float64bits(gotLon) != body.LonBits {
t.Fatalf("%s lon regression at %s", body.Name, sample.UTC)
}
gotLonN := planet.WherePlanetN(body.XT, 0, sample.TTJD, -1)
if math.Float64bits(gotLonN) != body.LonBits {
t.Fatalf("%s lon full-n regression at %s", body.Name, sample.UTC)
}
gotLat := planet.WherePlanet(body.XT, 1, sample.TTJD)
if math.Float64bits(gotLat) != body.LatBits {
t.Fatalf("%s lat regression at %s", body.Name, sample.UTC)
}
gotLatN := planet.WherePlanetN(body.XT, 1, sample.TTJD, -1)
if math.Float64bits(gotLatN) != body.LatBits {
t.Fatalf("%s lat full-n regression at %s", body.Name, sample.UTC)
}
gotRad := planet.WherePlanet(body.XT, 2, sample.TTJD)
if math.Float64bits(gotRad) != body.RadBits {
t.Fatalf("%s rad regression at %s", body.Name, sample.UTC)
}
gotRadN := planet.WherePlanetN(body.XT, 2, sample.TTJD, -1)
if math.Float64bits(gotRadN) != body.RadBits {
t.Fatalf("%s rad full-n regression at %s", body.Name, sample.UTC)
}
}
if math.Float64bits(basic.HMoonTrueLo(sample.TTJD)) != sample.Moon.LonBits {
t.Fatalf("moon lon regression at %s", sample.UTC)
}
if math.Float64bits(basic.HMoonTrueLoN(sample.TTJD, -1)) != sample.Moon.LonBits {
t.Fatalf("moon lon full-n regression at %s", sample.UTC)
}
if math.Float64bits(basic.HMoonTrueBo(sample.TTJD)) != sample.Moon.LatBits {
t.Fatalf("moon lat regression at %s", sample.UTC)
}
if math.Float64bits(basic.HMoonTrueBoN(sample.TTJD, -1)) != sample.Moon.LatBits {
t.Fatalf("moon lat full-n regression at %s", sample.UTC)
}
if math.Float64bits(basic.HMoonAway(sample.TTJD)) != sample.Moon.DisBits {
t.Fatalf("moon distance regression at %s", sample.UTC)
}
if math.Float64bits(basic.HMoonAwayN(sample.TTJD, -1)) != sample.Moon.DisBits {
t.Fatalf("moon distance full-n regression at %s", sample.UTC)
}
}
}
func TestPublicTruncationFullMatchesDefault(t *testing.T) {
date := time.Date(2026, 1, 2, 3, 4, 5, 123456789, time.UTC)
if math.Float64bits(sun.TrueLo(date)) != math.Float64bits(sun.TrueLoN(date, -1)) {
t.Fatal("sun.TrueLoN(-1) should match default")
}
if math.Float64bits(sun.TrueBo(date)) != math.Float64bits(sun.TrueBoN(date, -1)) {
t.Fatal("sun.TrueBoN(-1) should match default")
}
if math.Float64bits(moon.TrueLo(date)) != math.Float64bits(moon.TrueLoN(date, -1)) {
t.Fatal("moon.TrueLoN(-1) should match default")
}
if math.Float64bits(moon.TrueBo(date)) != math.Float64bits(moon.TrueBoN(date, -1)) {
t.Fatal("moon.TrueBoN(-1) should match default")
}
}
-39
View File
@@ -1,39 +0,0 @@
package basic
import (
"fmt"
"testing"
)
func Test_All(t *testing.T) {
show()
}
func Benchmark_All(b *testing.B) {
for i := 0; i < b.N; i++ {
show()
}
}
func show() {
jde := GetNowJDE() - 1
ra := HSunApparentRa(jde - 8.0/24.0)
dec := HSunApparentDec(jde - 8.0/24.0)
fmt.Printf("当前JDE:%.14f\n", jde)
fmt.Println("当前太阳黄经:", HSunApparentLo(jde-8.0/24.0))
fmt.Println("当前太阳赤经:", ra)
fmt.Println("当前太阳赤纬:", dec)
fmt.Println("当前太阳星座:", WhichCst(ra, dec, jde))
fmt.Println("当前黄赤交角:", EclipticObliquity(jde-8.0/24.0, true))
fmt.Println("当前日出:", JDE2Date(GetSunRiseTime(jde, 115, 32, 8, 1, 10)))
fmt.Println("当前日落:", JDE2Date(GetSunSetTime(jde, 115, 32, 8, 1, 10)))
fmt.Println("当前晨影 -6", JDE2Date(MorningTwilight(jde, 115, 32, 8, -6)))
fmt.Println("当前晨影 -12", JDE2Date(MorningTwilight(jde, 115, 32, 8, -12)))
fmt.Println("当前昏影 -6", JDE2Date(EveningTwilight(jde, 115, 32, 8, -6)))
fmt.Println("当前昏影 -12", JDE2Date(EveningTwilight(jde, 115, 32, 8, -12)))
fmt.Print("农历:")
fmt.Println(GetLunar(2019, 10, 23, 8.0/24.0))
fmt.Println("当前月出:", JDE2Date(GetMoonRiseTime(jde, 115, 32, 8, 1, 10)))
fmt.Println("当前月落:", JDE2Date(GetMoonSetTime(jde, 115, 32, 8, 1, 10)))
fmt.Println("月相:", MoonPhase(jde-8.0/24.0))
}
+253
View File
@@ -0,0 +1,253 @@
package basic
import (
"math"
"sort"
"time"
)
const (
earthApsisBaseTTJDE = 2451547.507
earthApsisMeanYearDays = 365.2596358
earthApsisQuadraticTerm = 0.0000000156
earthApsisBaseYear = 2000.01
earthApsisSeedScale = 0.99997
earthApsisBracketHalfWidth = 5.0
earthApsisSampleStep = 0.25
earthApsisDerivativeStep = 1e-3
earthApsisToleranceDays = 1e-8
earthApsisMaxIterations = 24
moonApsisBaseTTJDE = 2451534.6698
moonApsisMeanMonthDays = 27.55454989
moonApsisBaseCycle = 1325.55
moonApsisQuadraticTerm = -0.0006691
moonApsisCubicTerm = -0.000001098
moonApsisQuarticTerm = 0.0000000052
moonApsisBracketHalfWidth = 2.0
moonApsisSampleStep = 0.125
moonApsisDerivativeStep = 1e-4
moonApsisToleranceDays = 1e-8
moonApsisMaxIterations = 24
)
// ApsisEvent 轨道极值事件 / orbital distance extremum event.
type ApsisEvent struct {
// JDE 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JDE float64
// Distance 是极值距离;地球相关事件单位 AU,月球相关事件单位 km / extremum distance.
Distance float64
}
type apsisSearchConfig struct {
bracketHalfWidth float64
sampleStep float64
derivativeStep float64
toleranceDays float64
maxIterations int
maximize bool
}
// EarthPerihelion 地球指定年份的近日点 / Earth perihelion in the given year.
func EarthPerihelion(year int) ApsisEvent {
return earthApsis(year, false)
}
// EarthAphelion 地球指定年份的远日点 / Earth aphelion in the given year.
func EarthAphelion(year int) ApsisEvent {
return earthApsis(year, true)
}
// MoonPerigees 指定年月内的所有月球近地点 / all lunar perigees in the given Gregorian month.
func MoonPerigees(year int, month time.Month) []ApsisEvent {
return moonApsisInMonth(year, month, false)
}
// MoonApogees 指定年月内的所有月球远地点 / all lunar apogees in the given Gregorian month.
func MoonApogees(year int, month time.Month) []ApsisEvent {
return moonApsisInMonth(year, month, true)
}
func earthApsis(year int, aphelion bool) ApsisEvent {
seedTT := earthApsisSeedTT(year, aphelion)
cfg := apsisSearchConfig{
bracketHalfWidth: earthApsisBracketHalfWidth,
sampleStep: earthApsisSampleStep,
derivativeStep: earthApsisDerivativeStep,
toleranceDays: earthApsisToleranceDays,
maxIterations: earthApsisMaxIterations,
maximize: aphelion,
}
eventTT, distanceAU := refineDistanceExtremum(seedTT, cfg, EarthAway)
return ApsisEvent{
JDE: TD2UT(eventTT, false),
Distance: distanceAU,
}
}
func moonApsisInMonth(year int, month time.Month, apogee bool) []ApsisEvent {
startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
endUTC := startUTC.AddDate(0, 1, 0)
startTT := TD2UT(Date2JDE(startUTC), true)
endTT := TD2UT(Date2JDE(endUTC), true)
kStart := int(math.Floor((startTT-moonApsisBaseTTJDE)/moonApsisMeanMonthDays)) - 1
kEnd := int(math.Ceil((endTT-moonApsisBaseTTJDE)/moonApsisMeanMonthDays)) + 1
phase := 0.0
if apogee {
phase = 0.5
}
cfg := apsisSearchConfig{
bracketHalfWidth: moonApsisBracketHalfWidth,
sampleStep: moonApsisSampleStep,
derivativeStep: moonApsisDerivativeStep,
toleranceDays: moonApsisToleranceDays,
maxIterations: moonApsisMaxIterations,
maximize: apogee,
}
events := make([]ApsisEvent, 0, 2)
for k := kStart; k <= kEnd; k++ {
seedTT := moonApsisSeedTT(float64(k) + phase)
eventTT, distanceKM := refineDistanceExtremum(seedTT, cfg, HMoonAway)
eventUT := TD2UT(eventTT, false)
eventTimeUTC := JDE2DateByZone(eventUT, time.UTC, false)
if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) {
continue
}
events = append(events, ApsisEvent{
JDE: eventUT,
Distance: distanceKM,
})
}
sort.Slice(events, func(i, j int) bool {
return events[i].JDE < events[j].JDE
})
return events
}
func earthApsisSeedTT(year int, aphelion bool) float64 {
k := math.Round(earthApsisSeedScale * (float64(year) - earthApsisBaseYear))
if aphelion {
k += 0.5
}
return earthApsisBaseTTJDE + earthApsisMeanYearDays*k + earthApsisQuadraticTerm*k*k
}
func moonApsisSeedTT(k float64) float64 {
t := k / moonApsisBaseCycle
return moonApsisBaseTTJDE +
moonApsisMeanMonthDays*k +
moonApsisQuadraticTerm*t*t +
moonApsisCubicTerm*t*t*t +
moonApsisQuarticTerm*t*t*t*t
}
func refineDistanceExtremum(seed float64, cfg apsisSearchConfig, distanceFn func(float64) float64) (float64, float64) {
best := seed
bestDistance := distanceFn(seed)
for sample := seed - cfg.bracketHalfWidth; sample <= seed+cfg.bracketHalfWidth+1e-12; sample += cfg.sampleStep {
dist := distanceFn(sample)
if distanceBetter(dist, bestDistance, cfg.maximize) {
best = sample
bestDistance = dist
}
}
left, right, ok := apsisDerivativeBracket(best, seed, cfg, distanceFn)
if !ok {
return best, bestDistance
}
leftDeriv := apsisDistanceDerivative(distanceFn, left, cfg.derivativeStep)
rightDeriv := apsisDistanceDerivative(distanceFn, right, cfg.derivativeStep)
current := best
for i := 0; i < cfg.maxIterations; i++ {
first, second := apsisDistanceDerivatives(distanceFn, current, cfg.derivativeStep)
next := current
if math.Abs(second) > 0 {
next = current - first/second
}
if !(next > left && next < right) || math.IsNaN(next) || math.IsInf(next, 0) {
next = (left + right) / 2
}
nextDeriv := apsisDistanceDerivative(distanceFn, next, cfg.derivativeStep)
if leftDeriv == 0 {
right = next
rightDeriv = nextDeriv
} else if leftDeriv*nextDeriv <= 0 {
right = next
rightDeriv = nextDeriv
} else {
left = next
leftDeriv = nextDeriv
}
if math.Abs(next-current) <= cfg.toleranceDays || math.Abs(right-left) <= cfg.toleranceDays {
current = next
break
}
current = next
_ = rightDeriv
}
return current, distanceFn(current)
}
func apsisDerivativeBracket(best, seed float64, cfg apsisSearchConfig, distanceFn func(float64) float64) (float64, float64, bool) {
leftBound := seed - cfg.bracketHalfWidth
rightBound := seed + cfg.bracketHalfWidth
left := best - cfg.sampleStep
right := best + cfg.sampleStep
if left < leftBound {
left = leftBound
}
if right > rightBound {
right = rightBound
}
leftDeriv := apsisDistanceDerivative(distanceFn, left, cfg.derivativeStep)
rightDeriv := apsisDistanceDerivative(distanceFn, right, cfg.derivativeStep)
for i := 0; i < cfg.maxIterations; i++ {
if leftDeriv == 0 || rightDeriv == 0 || leftDeriv*rightDeriv < 0 {
return left, right, true
}
if left > leftBound {
left -= cfg.sampleStep
if left < leftBound {
left = leftBound
}
leftDeriv = apsisDistanceDerivative(distanceFn, left, cfg.derivativeStep)
}
if right < rightBound {
right += cfg.sampleStep
if right > rightBound {
right = rightBound
}
rightDeriv = apsisDistanceDerivative(distanceFn, right, cfg.derivativeStep)
}
}
return 0, 0, false
}
func apsisDistanceDerivative(distanceFn func(float64) float64, jd, h float64) float64 {
return (distanceFn(jd+h) - distanceFn(jd-h)) / (2 * h)
}
func apsisDistanceDerivatives(distanceFn func(float64) float64, jd, h float64) (float64, float64) {
prev := distanceFn(jd - h)
curr := distanceFn(jd)
next := distanceFn(jd + h)
first := (next - prev) / (2 * h)
second := (next - 2*curr + prev) / (h * h)
return first, second
}
func distanceBetter(candidate, current float64, maximize bool) bool {
if maximize {
return candidate > current
}
return candidate < current
}
+175
View File
@@ -0,0 +1,175 @@
package basic
import (
"encoding/json"
"math"
"os"
"testing"
"time"
)
type earthApsisSample struct {
Kind string `json:"kind"`
Year int `json:"year"`
TimeUTC string `json:"time_utc"`
DistanceAU float64 `json:"distance_au"`
}
type moonApsisSample struct {
Kind string `json:"kind"`
Year int `json:"year"`
Month int `json:"month"`
TimeUTC string `json:"time_utc"`
DistanceKM float64 `json:"distance_km"`
}
type moonApsisMonthState struct {
perigees []ApsisEvent
apogees []ApsisEvent
perigeeI int
apogeeI int
}
func TestEarthApsisMatchesHorizonsBaseline(t *testing.T) {
data, err := os.ReadFile("testdata/earth_apsis_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []earthApsisSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
const timeTolerance = 2 * time.Minute
const distanceToleranceAU = 5e-8
var maxTimeDiff time.Duration
var maxDistanceDiff float64
for _, sample := range samples {
wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
}
var got ApsisEvent
switch sample.Kind {
case "perihelion":
got = EarthPerihelion(sample.Year)
case "aphelion":
got = EarthAphelion(sample.Year)
default:
t.Fatalf("unknown earth apsis kind %q", sample.Kind)
}
gotTime := JDE2DateByZone(got.JDE, time.UTC, false)
timeDiff := gotTime.Sub(wantTime)
if timeDiff < 0 {
timeDiff = -timeDiff
}
if timeDiff > maxTimeDiff {
maxTimeDiff = timeDiff
}
if timeDiff > timeTolerance {
t.Fatalf("%s %d time mismatch: got %s want %s tolerance %v", sample.Kind, sample.Year, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, timeTolerance)
}
distanceDiff := math.Abs(got.Distance - sample.DistanceAU)
if distanceDiff > maxDistanceDiff {
maxDistanceDiff = distanceDiff
}
if distanceDiff > distanceToleranceAU {
t.Fatalf("%s %d distance mismatch: got %.12f want %.12f tolerance %.12f", sample.Kind, sample.Year, got.Distance, sample.DistanceAU, distanceToleranceAU)
}
}
t.Logf("earth apsis max diff: time=%v distance=%.12f AU", maxTimeDiff, maxDistanceDiff)
}
func TestMoonApsisMatchesHorizonsBaseline(t *testing.T) {
// Baseline is generated from JPL Horizons by scripts/generate_moon_apsis_baseline.sh.
data, err := os.ReadFile("testdata/moon_apsis_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []moonApsisSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
const timeTolerance = 20 * time.Minute
const distanceToleranceKM = 50.0
states := make(map[int]*moonApsisMonthState)
var maxTimeDiff time.Duration
var maxDistanceDiff float64
for _, sample := range samples {
wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
}
key := sample.Year*100 + sample.Month
state := states[key]
if state == nil {
state = &moonApsisMonthState{
perigees: MoonPerigees(sample.Year, time.Month(sample.Month)),
apogees: MoonApogees(sample.Year, time.Month(sample.Month)),
}
states[key] = state
}
var got ApsisEvent
switch sample.Kind {
case "perigee":
if state.perigeeI >= len(state.perigees) {
t.Fatalf("%04d-%02d missing perigee #%d", sample.Year, sample.Month, state.perigeeI+1)
}
got = state.perigees[state.perigeeI]
state.perigeeI++
case "apogee":
if state.apogeeI >= len(state.apogees) {
t.Fatalf("%04d-%02d missing apogee #%d", sample.Year, sample.Month, state.apogeeI+1)
}
got = state.apogees[state.apogeeI]
state.apogeeI++
default:
t.Fatalf("unknown moon apsis kind %q", sample.Kind)
}
gotTime := JDE2DateByZone(got.JDE, time.UTC, false)
timeDiff := gotTime.Sub(wantTime)
if timeDiff < 0 {
timeDiff = -timeDiff
}
if timeDiff > maxTimeDiff {
maxTimeDiff = timeDiff
}
if timeDiff > timeTolerance {
t.Fatalf("%s %04d-%02d time mismatch: got %s want %s tolerance %v", sample.Kind, sample.Year, sample.Month, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, timeTolerance)
}
distanceDiff := math.Abs(got.Distance - sample.DistanceKM)
if distanceDiff > maxDistanceDiff {
maxDistanceDiff = distanceDiff
}
if distanceDiff > distanceToleranceKM {
t.Fatalf("%s %04d-%02d distance mismatch: got %.6f want %.6f tolerance %.6f", sample.Kind, sample.Year, sample.Month, got.Distance, sample.DistanceKM, distanceToleranceKM)
}
}
for key, state := range states {
year := key / 100
month := key % 100
if state.perigeeI != len(state.perigees) {
t.Fatalf("%04d-%02d unconsumed perigees: got %d of %d", year, month, state.perigeeI, len(state.perigees))
}
if state.apogeeI != len(state.apogees) {
t.Fatalf("%04d-%02d unconsumed apogees: got %d of %d", year, month, state.apogeeI, len(state.apogees))
}
}
t.Logf("moon apsis max diff: time=%v distance=%.6f km", maxTimeDiff, maxDistanceDiff)
}
-450
View File
@@ -1,450 +0,0 @@
package basic
import (
"fmt"
"math"
"strings"
"time"
)
var defDeltaTFn = DefaultDeltaTv2
// Date2JDE 日期转儒略日
func Date2JDE(date time.Time) float64 {
day := float64(date.Day()) + float64(date.Hour())/24.0 + float64(date.Minute())/24.0/60.0 + float64(date.Second())/24.0/3600.0 + float64(date.Nanosecond())/1000000000.0/3600.0/24.0
return JDECalc(date.Year(), int(date.Month()), day)
}
/*
@name: 儒略日计算
@dec: 计算给定时间的儒略日,1582年改力后为格里高利历,之前为儒略历
@ 请注意,传入的时间在天文计算中一般为力学时,应当注意和世界时的转化
*/
func JDECalc(Year, Month int, Day float64) float64 {
if Month == 1 || Month == 2 {
Year--
Month += 12
}
var tmpvarB int
tmpvar := fmt.Sprintf("%04d-%02d-%2d", Year, Month, int(math.Floor(Day)))
if strings.Compare(tmpvar, `1582-10-04`) != 1 {
tmpvarB = 0
} else {
tmpvarA := int(Year / 100)
tmpvarB = 2 - tmpvarA + int(tmpvarA/4)
}
return (math.Floor(365.25*(float64(Year)+4716.0)) + math.Floor(30.6001*float64(Month+1)) + Day + float64(tmpvarB) - 1524.5)
}
/*
@name: 获得当前儒略日时间:当地世界时,非格林尼治时间
*/
func GetNowJDE() (NowJDE float64) {
Time := float64(time.Now().Second())/3600.0/24.0 + float64(time.Now().Minute())/60.0/24.0 + float64(time.Now().Hour())/24.0
NowJDE = JDECalc(time.Now().Year(), int(time.Now().Month()), float64(time.Now().Day())+Time)
return
}
func DeltaT(date float64, isJDE bool) float64 {
return defDeltaTFn(date, isJDE)
}
func SetDeltaTFn(fn func(float64, bool) float64) {
if fn != nil {
defDeltaTFn = fn
}
}
func GetDeltaTFn() func(float64, bool) float64 {
return defDeltaTFn
}
func DefaultDeltaTv2(date float64, isJd bool) float64 { //传入年或儒略日,传出为秒
if !isJd {
date = JDECalc(int(date), int((date-math.Floor(date))*12)+1, (date-math.Floor(date))*365.25+1)
}
return DeltaTv2(date)
}
// 使用Stephenson等人(2016)和Morrison等人(2021)的拟合和外推公式计算Delta T
// http://astro.ukho.gov.uk/nao/lvm/
// 2010年后的系数已修改以包含2019年后的数据
// 返回Delta T,单位为秒
func DeltaTSplineY(y float64) float64 {
// 积分lod(平均太阳日偏离86400秒的偏差)方程:
// 来自 http://astro.ukho.gov.uk/nao/lvm/
// lod = 1.72 t 3.5 sin(2*pi*(t+0.75)/14) 单位ms/day,其中 t = (y - 1825)/100
// 是从1825年开始的世纪数
// 使用 1ms = 1e-3s 和 1儒略年 = 365.25天,
// lod = 6.2823e-3 * Delta y - 1.278375*sin(2*pi/14*(Delta y /100 + 0.75) 单位s/year
// 其中 Delta y = y - 1825。积分该方程得到
// Integrate[lod, y] = 3.14115e-3*(Delta y)^2 + 894.8625/pi*cos(2*pi/14*(Delta y /100 + 0.75)
// 单位为秒。积分常数设为0。
integratedLod := func(x float64) float64 {
u := x - 1825
return 3.14115e-3*u*u + 284.8435805251424*math.Cos(0.4487989505128276*(0.01*u+0.75))
}
if y < -720 {
// 使用积分lod + 常数
const c = 1.007739546148514
return integratedLod(y) + c
}
if y > 2025 {
// 使用积分lod + 常数
const c = -150.56787057979514
return integratedLod(y) + c
}
// 使用三次样条拟合
y0 := []float64{-720, -100, 400, 1000, 1150, 1300, 1500, 1600, 1650, 1720, 1800, 1810, 1820, 1830, 1840, 1850, 1855, 1860, 1865, 1870, 1875, 1880, 1885, 1890, 1895, 1900, 1905, 1910, 1915, 1920, 1925, 1930, 1935, 1940, 1945, 1950, 1953, 1956, 1959, 1962, 1965, 1968, 1971, 1974, 1977, 1980, 1983, 1986, 1989, 1992, 1995, 1998, 2001, 2004, 2007, 2010, 2013, 2016, 2019, 2022}
y1 := []float64{-100, 400, 1000, 1150, 1300, 1500, 1600, 1650, 1720, 1800, 1810, 1820, 1830, 1840, 1850, 1855, 1860, 1865, 1870, 1875, 1880, 1885, 1890, 1895, 1900, 1905, 1910, 1915, 1920, 1925, 1930, 1935, 1940, 1945, 1950, 1953, 1956, 1959, 1962, 1965, 1968, 1971, 1974, 1977, 1980, 1983, 1986, 1989, 1992, 1995, 1998, 2001, 2004, 2007, 2010, 2013, 2016, 2019, 2022, 2025}
a0 := []float64{20371.848, 11557.668, 6535.116, 1650.393, 1056.647, 681.149, 292.343, 109.127, 43.952, 12.068, 18.367, 15.678, 16.516, 10.804, 7.634, 9.338, 10.357, 9.04, 8.255, 2.371, -1.126, -3.21, -4.388, -3.884, -5.017, -1.977, 4.923, 11.142, 17.479, 21.617, 23.789, 24.418, 24.164, 24.426, 27.05, 28.932, 30.002, 30.76, 32.652, 33.621, 35.093, 37.956, 40.951, 44.244, 47.291, 50.361, 52.936, 54.984, 56.373, 58.453, 60.678, 62.898, 64.083, 64.553, 65.197, 66.061, 66.919, 68.130, 69.250, 69.296}
a1 := []float64{-9999.586, -5822.27, -5671.519, -753.21, -459.628, -421.345, -192.841, -78.697, -68.089, 2.507, -3.481, 0.021, -2.157, -6.018, -0.416, 1.642, -0.486, -0.591, -3.456, -5.593, -2.314, -1.893, 0.101, -0.531, 0.134, 5.715, 6.828, 6.33, 5.518, 3.02, 1.333, 0.052, -0.419, 1.645, 2.499, 1.127, 0.737, 1.409, 1.577, 0.868, 2.275, 3.035, 3.157, 3.199, 3.069, 2.878, 2.354, 1.577, 1.648, 2.235, 2.324, 1.804, 0.674, 0.466, 0.804, 0.839, 1.005, 1.348, 0.594, -0.227}
a2 := []float64{776.247, 1303.151, -298.291, 184.811, 108.771, 61.953, -6.572, 10.505, 38.333, 41.731, -1.126, 4.629, -6.806, 2.944, 2.658, 0.261, -2.389, 2.284, -5.148, 3.011, 0.269, 0.152, 1.842, -2.474, 3.138, 2.443, -1.329, 0.831, -1.643, -0.856, -0.831, -0.449, -0.022, 2.086, -1.232, 0.22, -0.61, 1.282, -1.115, 0.406, 1.002, -0.242, 0.364, -0.323, 0.193, -0.384, -0.14, -0.637, 0.708, -0.121, 0.21, -0.729, -0.402, 0.194, 0.144, -0.109, 0.275, 0.068, -0.822, 0.001}
a3 := []float64{409.16, -503.433, 1085.087, -25.346, -24.641, -29.414, 16.197, 3.018, -2.127, -37.939, 1.918, -3.812, 3.25, -0.096, -0.539, -0.883, 1.558, -2.477, 2.72, -0.914, -0.039, 0.563, -1.438, 1.871, -0.232, -1.257, 0.72, -0.825, 0.262, 0.008, 0.127, 0.142, 0.702, -1.106, 0.614, -0.277, 0.631, -0.799, 0.507, 0.199, -0.414, 0.202, -0.229, 0.172, -0.192, 0.081, -0.165, 0.448, -0.276, 0.11, -0.313, 0.109, 0.199, -0.017, -0.084, 0.128, -0.069, -0.297, 0.274, 0.086}
n := len(y0)
var i int
for i = n - 1; i >= 0; i-- {
if y >= y0[i] {
break
}
}
t := (y - y0[i]) / (y1[i] - y0[i])
dT := a0[i] + t*(a1[i]+t*(a2[i]+t*a3[i]))
return dT
}
func DeltaTv2(jd float64) float64 {
if jd > 2461041.5 || jd < 2441317.5 {
var y float64
if jd >= 2299160.5 {
y = (jd-2451544.5)/365.2425 + 2000
} else {
y = (jd+0.5)/365.25 - 4712
}
return DeltaTSplineY(y)
}
// 闰秒JD值
jdLeaps := []float64{2457754.5, 2457204.5, 2456109.5, 2454832.5,
2453736.5, 2451179.5, 2450630.5, 2450083.5,
2449534.5, 2449169.5, 2448804.5, 2448257.5,
2447892.5, 2447161.5, 2446247.5, 2445516.5,
2445151.5, 2444786.5, 2444239.5, 2443874.5,
2443509.5, 2443144.5, 2442778.5, 2442413.5,
2442048.5, 2441683.5, 2441499.5, 2441133.5}
n := len(jdLeaps)
DT := 42.184
for i := 0; i < n; i++ {
if jd > jdLeaps[i] {
DT += float64(n - i - 1)
break
}
}
return DT
}
func TD2UT(JDE float64, UT2TD bool) float64 { // true 世界时转力学时CC,false 力学时转世界时VV
Deltat := DeltaT(JDE, true)
if UT2TD {
return JDE + Deltat/3600/24
} else {
return JDE - Deltat/3600/24
}
}
func JDE2Date(JD float64) time.Time {
JD = JD + 0.5
Z := float64(int(JD))
F := JD - Z
var A, B, Years, Months, Days float64
if Z < 2299161.0 {
A = Z
} else {
alpha := math.Floor((Z - 1867216.25) / 36524.25)
A = Z + 1 + alpha - math.Floor(alpha/4)
}
B = A + 1524
C := math.Floor((B - 122.1) / 365.25)
D := math.Floor(365.25 * C)
E := math.Floor((B - D) / 30.6001)
Days = B - D - math.Floor(30.6001*E) + F
if E < 14 {
Months = E - 1
}
if E == 14 || E == 15 {
Months = E - 13
}
if Months > 2 {
Years = C - 4716
}
if Months == 1 || Months == 2 {
Years = C - 4715
}
tms := (Days - math.Floor(Days)) * 24 * 3600
Days = math.Floor(Days)
tz, _ := time.LoadLocation("Local")
dates := time.Date(int(Years), time.Month(int(Months)), int(Days), 0, 0, 0, 0, tz)
return time.Unix(dates.Unix()+int64(tms), int64((tms-math.Floor(tms))*1000000000))
}
// JDE2DateByZone JDE(儒略日)转日期
// JD: 儒略日
// tz: 目标时区
// byZone: (true: 传入的儒略日视为目标时区当地时间的儒略日,false: 传入的儒略日视为UTC时间的儒略日)
// 回参:转换后的日期,时区始终为目标时区
func JDE2DateByZone(JD float64, tz *time.Location, byZone bool) time.Time {
JD = JD + 0.5
Z := float64(int(JD))
F := JD - Z
var A, B, Years, Months, Days float64
if Z < 2299161.0 {
A = Z
} else {
alpha := math.Floor((Z - 1867216.25) / 36524.25)
A = Z + 1 + alpha - math.Floor(alpha/4)
}
B = A + 1524
C := math.Floor((B - 122.1) / 365.25)
D := math.Floor(365.25 * C)
E := math.Floor((B - D) / 30.6001)
Days = B - D - math.Floor(30.6001*E) + F
if E < 14 {
Months = E - 1
}
if E == 14 || E == 15 {
Months = E - 13
}
if Months > 2 {
Years = C - 4716
}
if Months == 1 || Months == 2 {
Years = C - 4715
}
tms := (Days - math.Floor(Days)) * 24 * 3600
Days = math.Floor(Days)
var transTz = tz
if !byZone {
transTz = time.UTC
}
return time.Date(int(Years), time.Month(int(Months)), int(Days), 0, 0, 0, 0, transTz).
Add(time.Duration(int64(1000000000 * tms))).In(tz)
}
func GetLunar(year, month, day int, tz float64) (lyear, lmonth, lday int, leap bool, result string) {
julianDayEpoch := JDECalc(year, month, float64(day))
// 确定农历年份
lyear = year
adjustedYear := year
if month == 11 || month == 12 {
winterSolsticeDay := GetJQTime(year, 270) + tz
//firstNewMoonDay := TD2UT(CalcMoonS(float64(year)+11.0/12.0+5.0/30.0/12.0, 0), true) + tz
//nextNewMoonDay := TD2UT(CalcMoonS(float64(year)+1.0, 0), true) + tz
firstNewMoonDay := TD2UT(CalcMoonSHByJDE(winterSolsticeDay-16, 0), false) + tz
nextNewMoonDay := TD2UT(CalcMoonSHByJDE(firstNewMoonDay+28, 0), false) + tz
firstNewMoonDay = normalizeTimePoint(firstNewMoonDay)
nextNewMoonDay = normalizeTimePoint(nextNewMoonDay)
if winterSolsticeDay >= firstNewMoonDay && winterSolsticeDay < nextNewMoonDay && julianDayEpoch < firstNewMoonDay {
adjustedYear--
}
if winterSolsticeDay >= nextNewMoonDay && julianDayEpoch < nextNewMoonDay {
adjustedYear--
}
} else {
adjustedYear--
}
// 获取节气和朔望月数据
solarTerms := GetJieqiLoops(adjustedYear, 25)
newMoonDays := GetMoonLoops(float64(adjustedYear), 17)
// 计算冬至日期
winterSolsticeFirst := normalizeTimePoint(solarTerms[0] - 8.0/24 + tz)
winterSolsticeSecond := normalizeTimePoint(solarTerms[24] - 8.0/24 + tz)
// 规范化时间点
normalizeTimeArray(newMoonDays, tz)
normalizeTimeArray(solarTerms, tz)
// 计算朔望月范围
minMoonIndex, maxMoonIndex := 20, 0
moonCount := 0
for i := 0; i < len(newMoonDays)-1; i++ {
if (newMoonDays[i] <= winterSolsticeFirst && newMoonDays[i+1] > winterSolsticeFirst) ||
(newMoonDays[i] > winterSolsticeFirst && newMoonDays[i] < winterSolsticeSecond && newMoonDays[i+1] <= winterSolsticeSecond) {
if i <= minMoonIndex {
minMoonIndex = i
}
if i >= maxMoonIndex {
maxMoonIndex = i
}
moonCount++
}
}
// 确定闰月位置
leapMonthPos := 20
if moonCount >= 13 {
solarTermIndex, i := 0, 0
for i = minMoonIndex; i <= maxMoonIndex; i++ {
if !(newMoonDays[i] <= solarTerms[solarTermIndex] && newMoonDays[i+1] > solarTerms[solarTermIndex]) {
break
}
solarTermIndex += 2
}
leapMonthPos = i - minMoonIndex
}
// 找到当前月相索引
currentMoonIndex := 0
for currentMoonIndex = minMoonIndex; currentMoonIndex <= maxMoonIndex; currentMoonIndex++ {
if newMoonDays[currentMoonIndex] > julianDayEpoch {
break
}
}
// 计算农历月份
lmonth = currentMoonIndex - minMoonIndex - 1
shouldAdjustLeap := false
leap = false
if lmonth >= leapMonthPos {
shouldAdjustLeap = true
}
if lmonth == leapMonthPos {
leap = true
}
if lmonth < 2 {
lmonth += 11
} else {
lmonth--
}
if shouldAdjustLeap {
lmonth--
}
if lmonth <= 0 {
lmonth += 12
}
// 计算农历日期
lday = int(julianDayEpoch-newMoonDays[currentMoonIndex-1]) + 1
// 生成农历日期字符串
result = formatLunarDateString(lmonth, lday, leap)
if lmonth >= 10 && month < 3 {
lyear--
}
return
}
func GetSolar(year, month, day int, leap bool, tz float64) float64 {
adjustedYear := year
if month < 11 {
adjustedYear--
}
// 获取节气和朔望月数据
solarTerms := GetJieqiLoops(adjustedYear, 25)
newMoonDays := GetMoonLoops(float64(adjustedYear), 17)
// 计算冬至日期
winterSolsticeFirst := normalizeTimePoint(solarTerms[0] - 8.0/24 + tz)
winterSolsticeSecond := normalizeTimePoint(solarTerms[24] - 8.0/24 + tz)
// 规范化时间点
normalizeTimeArray(newMoonDays, tz)
normalizeTimeArray(solarTerms, tz)
// 计算朔望月范围
minMoonIndex, maxMoonIndex := 20, 0
moonCount := 0
for i := 0; i < 15; i++ {
if (newMoonDays[i] <= winterSolsticeFirst && newMoonDays[i+1] > winterSolsticeFirst) ||
(newMoonDays[i] > winterSolsticeFirst && newMoonDays[i] < winterSolsticeSecond && newMoonDays[i+1] <= winterSolsticeSecond) {
if i <= minMoonIndex {
minMoonIndex = i
}
if i >= maxMoonIndex {
maxMoonIndex = i
}
moonCount++
}
}
// 确定闰月位置
leapMonthPos := 20
if moonCount >= 13 {
solarTermIndex, i := 0, 0
for i = minMoonIndex; i <= maxMoonIndex; i++ {
if !(newMoonDays[i] <= solarTerms[solarTermIndex] && newMoonDays[i+1] > solarTerms[solarTermIndex]) {
break
}
solarTermIndex += 2
}
leapMonthPos = i - minMoonIndex
}
actualMonth := month
if actualMonth > 10 {
actualMonth -= 11
} else {
actualMonth++
}
// 计算实际月份索引
if leap {
actualMonth++
}
if actualMonth >= leapMonthPos && !leap {
actualMonth++
}
return newMoonDays[minMoonIndex+actualMonth] + float64(day) - 1
}
func normalizeTimeArray(timeArray []float64, tz float64) {
for idx, timeValue := range timeArray {
adjustedTime := timeValue
if tz != 8.0/24 {
adjustedTime = timeValue - 8.0/24 + tz
}
timeArray[idx] = normalizeTimePoint(adjustedTime)
}
}
func normalizeTimePoint(timePoint float64) float64 {
if timePoint-math.Floor(timePoint) > 0.5 {
return math.Floor(timePoint) + 0.5
}
return math.Floor(timePoint) - 0.5
}
func formatLunarDateString(lunarMonth, lunarDay int, isLeap bool) string {
monthNames := []string{"十", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊"}
dayPrefixes := []string{"初", "十", "廿", "三"}
var dateString string
if isLeap {
dateString += "闰"
}
if lunarMonth == 1 {
dateString += "正月"
} else {
dateString += monthNames[lunarMonth] + "月"
}
if lunarDay == 20 {
dateString += "二十"
} else if lunarDay == 10 {
dateString += "初十"
} else {
dateString += dayPrefixes[lunarDay/10] + monthNames[lunarDay%10]
}
return dateString
}
+32 -211
View File
@@ -1,249 +1,70 @@
package basic
import (
"encoding/json"
"fmt"
"math"
"os"
"testing"
)
func TestGenerateMagic(t *testing.T) {
generateMagicNumber()
}
func generateMagicNumber() {
//0月份 00000 日期 0000闰月 0000000000000 农历信息
var tz = 8.0000 / 24.000
yearMap := make(map[int][][]int) // {1 month,1 leap,2 29/30}
spYear := make(map[int][]int)
var upper []uint16
var lower []uint16
var full []uint32
//var info uint32 = 0
for year := 1899; year <= 2401; year++ {
fmt.Println(year)
jieqi := GetJieqiLoops(year, 25) //一年的节气
moon := GetMoonLoops(float64(year), 17) //一年朔月日
winter1 := jieqi[0] - 8.0/24 + tz //第一年冬至日
winter2 := jieqi[24] - 8.0/24 + tz //第二年冬至日
for idx, v := range moon {
if tz != 8.0/24 {
v = v - 8.0/24 + tz
}
if v-math.Floor(v) > 0.5 {
moon[idx] = math.Floor(v) + 0.5
} else {
moon[idx] = math.Floor(v) - 0.5
}
} //置闰月为0点
for idx, v := range jieqi {
if tz != 8.0/24 {
v = v - 8.0/24 + tz
}
if v-math.Floor(v) > 0.5 {
jieqi[idx] = math.Floor(v) + 0.5
} else {
jieqi[idx] = math.Floor(v) - 0.5
}
} //置节气为0点
mooncount := 0 //年内朔望月计数
var min, max int = 20, 0 //最大最小计数
for i := 0; i < 15; i++ {
if moon[i] >= winter1 && moon[i] < winter2 {
if i <= min {
min = i
}
if i >= max {
max = i
}
mooncount++
}
}
leapmonth := 20
if mooncount == 13 { //存在闰月
var j, i = 2, 0
for i = min; i <= max; i++ {
if !(moon[i] <= jieqi[j] && moon[i+1] > jieqi[j]) {
break
}
j += 2
}
leapmonth = i - min + 1
}
month := 11
for idx := min; idx <= max; idx++ {
leap := 0
if idx != leapmonth {
month++
if month > 12 {
month -= 12
}
} else {
leap = 1
}
if leap == 0 && month == 1 {
cp := JDE2Date(moon[idx])
spYear[year+1] = append(spYear[year+1], []int{int(cp.Month()), cp.Day()}...)
}
if idx < 6 && month > 10 {
yearMap[year] = append(yearMap[year], []int{month, leap, int(moon[idx+1] - moon[idx])})
} else {
yearMap[year+1] = append(yearMap[year+1], []int{month, leap, int(moon[idx+1] - moon[idx])})
}
}
}
for year := 1900; year <= 2400; year++ {
fmt.Println(year)
magic := magicNumber(yearMap[year], spYear[year])
up, low := magicNumberSpilt(magic)
upper = append(upper, up)
lower = append(lower, uint16(low))
full = append(full, uint32(magic))
}
res := make(map[string]interface{})
res["up"] = upper
res["low"] = lower
res["full"] = full
d, _ := json.Marshal(res)
os.WriteFile("test.json", d, 0644)
}
func magicNumber(y1 [][]int, y2 []int) int32 {
var res int32
res = int32(y2[1]) << 18
if y2[0] == 2 {
res = res | 0x800000
}
for idx, v := range y1 {
if v[2] == 30 {
res = res | (1 << (13 - idx))
}
if v[1] == 1 {
res = (res & 0xFC3FFF) | int32((v[0])<<14)
}
}
return res
}
func magicNumberSpilt(magic int32) (uint16, uint8) {
var upper uint16
var lower uint8
lower = uint8(magic)
upper = uint16(magic >> 8)
return upper, lower
}
func TestGetJQTime(t *testing.T) {
originalFunc := func(Year, Angle int) float64 { //节气时间
var j int = 1
var Day int
var tp float64
if Angle%2 == 0 {
Day = 18
originalFunc := func(year, angle int) float64 {
const iterations = 1
var day int
if angle%2 == 0 {
day = 18
} else {
Day = 3
day = 3
}
if Angle%10 != 0 {
tp = float64(Angle+15.0) / 30.0
month := 3.0
if angle%10 != 0 {
month += float64(angle+15) / 30.0
} else {
tp = float64(Angle) / 30.0
month += float64(angle) / 30.0
}
Month := 3 + tp
if Month > 12 {
Month -= 12
if month > 12 {
month -= 12
}
JD1 := JDECalc(int(Year), int(Month), float64(Day))
if Angle == 0 {
Angle = 360
jd := JDECalc(year, int(month), float64(day))
if angle == 0 {
angle = 360
}
for i := 0; i < j; i++ {
for i := 0; i < iterations; i++ {
for {
JD0 := JD1
stDegree := JQLospec(JD0, float64(Angle)) - float64(Angle)
stDegreep := (JQLospec(JD0+0.000005, float64(Angle)) - JQLospec(JD0-0.000005, float64(Angle))) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
jd0 := jd
slope := (JQLospec(jd0+0.000005, float64(angle)) - JQLospec(jd0-0.000005, float64(angle))) / 0.00001
jd = jd0 - (JQLospec(jd0, float64(angle))-float64(angle))/slope
if math.Abs(jd-jd0) <= 0.00001 {
break
}
}
JD1 -= 0.001
jd -= 0.001
}
JD1 += 0.001
return TD2UT(JD1, false)
jd += 0.001
return TD2UT(jd, false)
}
// 测试数据:年份从1900-2200抽样,角度覆盖关键值
testCases := []struct {
year, angle int
year int
angle int
}{
// 边界年份
{1900, 0}, {1900, 15}, {1900, 30}, {1900, 45}, {1900, 90},
{1900, 180}, {1900, 270}, {1900, 360},
// 中间年份抽样
{1900, 0}, {1900, 15}, {1900, 30}, {1900, 45}, {1900, 90}, {1900, 180}, {1900, 270}, {1900, 360},
{1950, 0}, {1950, 30}, {1950, 90}, {1950, 180}, {1950, 270},
{2000, 0}, {2000, 15}, {2000, 45}, {2000, 90}, {2000, 360},
{2023, 0}, {2023, 30}, {2023, 90}, {2023, 180}, {2023, 270},
// 未来年份抽样
{2100, 0}, {2100, 15}, {2100, 30}, {2100, 45}, {2100, 90},
{2100, 180}, {2100, 270}, {2100, 360},
{2100, 0}, {2100, 15}, {2100, 30}, {2100, 45}, {2100, 90}, {2100, 180}, {2100, 270}, {2100, 360},
{2200, 0}, {2200, 30}, {2200, 90}, {2200, 180}, {2200, 270},
}
// 执行测试
allPassed := true
for _, tc := range testCases {
originalResult := originalFunc(tc.year, tc.angle)
optimizedResult := GetJQTime(tc.year, tc.angle)
diff := math.Abs(originalResult - optimizedResult)
if diff > 1e-10 {
t.Errorf("测试失败: year=%d, angle=%d\n原始结果: %.15f\n优化结果: %.15f\n差异: %.15f",
t.Fatalf("GetJQTime mismatch: year=%d angle=%d original=%.15f optimized=%.15f diff=%.15f",
tc.year, tc.angle, originalResult, optimizedResult, diff)
allPassed = false
} else {
t.Logf("测试通过: year=%d, angle=%d, 结果: %.15f",
tc.year, tc.angle, optimizedResult)
}
}
if allPassed {
t.Log("所有测试用例通过!优化函数与原始函数结果完全一致")
}
}
func TestJQ(t *testing.T) {
fmt.Println(GetJQTime(-721, 15))
}
func TestCal6402(t *testing.T) {
var year = 6402
var tz = 8.00 / 24.00
winterSolsticeDay := GetJQTime(year, 270) + tz
firstNewMoonDay := TD2UT(CalcMoonSHByJDE(winterSolsticeDay-15, 0), false) + tz
nextNewMoonDay := TD2UT(CalcMoonSHByJDE(firstNewMoonDay+28, 0), false) + tz
fmt.Println(JDE2Date(firstNewMoonDay))
fmt.Println(JDE2Date(nextNewMoonDay))
fmt.Println(HSunTrueLo(TD2UT(nextNewMoonDay, false)))
fmt.Println(HMoonTrueLo(TD2UT(nextNewMoonDay, false)))
firstNewMoonDay = normalizeTimePoint(firstNewMoonDay)
nextNewMoonDay = normalizeTimePoint(nextNewMoonDay)
fmt.Println(JDE2Date(winterSolsticeDay))
fmt.Println(JDE2Date(GetSolar(1984, 10, 2, true, 8.0/24.0)))
fmt.Println(GetLunar(1992, 11, 24, 8.0/24.0))
fmt.Println(GetLunar(6402, 12, 24, 8.0/24.0))
for i := 1; i <= 12; i++ {
fmt.Print("6403", i, "24 ---- ")
fmt.Println(GetLunar(6403, i, 24, 8.0/24.0))
}
fmt.Println("-------")
for _, v := range GetMoonLoops(float64(2132), 17) {
fmt.Println(JDE2Date(v))
}
fmt.Println("-------")
for _, v := range GetJieqiLoops(2132, 25) {
fmt.Println(JDE2Date(v))
}
}
+204
View File
@@ -0,0 +1,204 @@
package basic
import (
. "b612.me/astro/tools"
)
type constellationPoint struct {
RA float64 //赤经
DEC float64 //赤纬
}
var constellationPolygons map[string][]constellationPoint
func initConstellationPolygons() {
constellationPolygons = make(map[string][]constellationPoint, 89)
constellationPolygons["AND"] = []constellationPoint{constellationPoint{344.46530375, 35.1682358}, constellationPoint{344.34285125, 53.1680298}, constellationPoint{351.45289375, 53.1870041}, constellationPoint{351.4656825, 50.6870193}, constellationPoint{355.27055708333, 50.6929131}, constellationPoint{355.27607875, 48.6929169}, constellationPoint{4.1463675, 48.6949348}, constellationPoint{4.14327875, 46.6949348}, constellationPoint{14.776077083333, 46.6757545}, constellationPoint{14.7888675, 48.6757393}, constellationPoint{18.588407916667, 48.6632690}, constellationPoint{18.60590375, 50.6632347}, constellationPoint{22.40793625, 50.6478767}, constellationPoint{26.96852375, 50.6257439}, constellationPoint{26.931439583333, 47.6258430}, constellationPoint{32.62149125, 47.5927505}, constellationPoint{32.67380125, 51.0925827}, constellationPoint{39.88547875, 51.0423737}, constellationPoint{39.67934125, 37.2931557}, constellationPoint{31.87109125, 37.3470840}, constellationPoint{31.854250416667, 35.5971375}, constellationPoint{22.910835, 35.6453362}, constellationPoint{22.89742625, 33.6453705}, constellationPoint{12.44306375, 33.6818962}, constellationPoint{12.41349125, 24.4319324}, constellationPoint{14.424064583333, 24.4266243}, constellationPoint{14.414815416667, 21.6766376}, constellationPoint{3.73992125, 21.6951923}, constellationPoint{3.7406445833333, 22.6951923}, constellationPoint{2.61001625, 22.6957588}, constellationPoint{2.6128425, 28.6957588}, constellationPoint{1.60621625, 28.6960354}, constellationPoint{1.6069770833333, 32.0293655}, constellationPoint{357.82874125, 32.0285034}, constellationPoint{357.8280825, 32.7785072}, constellationPoint{354.04915791667, 32.7746468}, constellationPoint{354.04417958333, 35.1913109}}
constellationPolygons["ANT"] = []constellationPoint{constellationPoint{141.904335, -24.5425186}, constellationPoint{141.77159875, -37.2920151}, constellationPoint{141.73406125, -40.2918739}, constellationPoint{166.45650458333, -40.4246216}, constellationPoint{166.47936291667, -35.6746559}, constellationPoint{163.95851541667, -35.6664963}, constellationPoint{163.977885, -31.8332005}, constellationPoint{160.20137875, -31.8185863}, constellationPoint{160.21289375, -29.8186131}, constellationPoint{155.18132708333, -29.7947845}, constellationPoint{155.1993375, -27.1281624}, constellationPoint{147.65928291667, -27.0835037}, constellationPoint{147.67968125, -24.5835705}}
constellationPolygons["APS"] = []constellationPoint{constellationPoint{209.11110875, -83.1200714}, constellationPoint{276.86599791667, -82.4582748}, constellationPoint{274.19506041667, -74.9745178}, constellationPoint{273.28007708333, -67.4800797}, constellationPoint{265.77572875, -67.5711060}, constellationPoint{258.2424825, -67.6610870}, constellationPoint{258.47067875, -70.1597443}, constellationPoint{224.16644125, -70.5115433}, constellationPoint{207.46087041667, -70.6244431}, constellationPoint{207.78143375, -75.6235962}}
constellationPolygons["AQR"] = []constellationPoint{constellationPoint{309.59884625, 0.4361772}, constellationPoint{309.5798775, 2.4360874}, constellationPoint{314.08109708333, 2.4773185}, constellationPoint{321.58347125, 2.5393796}, constellationPoint{323.58427041667, 2.5544112}, constellationPoint{323.57874875, 3.3043909}, constellationPoint{326.58021875, 3.3256676}, constellationPoint{326.58708625, 2.3256910}, constellationPoint{331.588755, 2.3576119}, constellationPoint{331.58726708333, 2.6076074}, constellationPoint{342.84221708333, 2.6622071}, constellationPoint{342.8497125, 0.6622211}, constellationPoint{342.86470375, -3.3377509}, constellationPoint{359.10221125, -3.3042023}, constellationPoint{359.10329875, -6.3042021}, constellationPoint{359.11056458333, -24.8042011}, constellationPoint{346.68096625, -24.8250446}, constellationPoint{329.77028875, -24.9040413}, constellationPoint{329.6561625, -8.4043999}, constellationPoint{321.668415, -8.4602947}, constellationPoint{321.71645125, -14.4601107}, constellationPoint{309.74390125, -14.5631361}, constellationPoint{309.68464791667, -8.5634165}}
constellationPolygons["AQL"] = []constellationPoint{constellationPoint{280.35020875, 0.1154895}, constellationPoint{280.3262325, 2.1153460}, constellationPoint{284.57642541667, 2.1659052}, constellationPoint{284.525985, 6.4156075}, constellationPoint{281.45856875, 6.3791943}, constellationPoint{281.388045, 12.1287737}, constellationPoint{284.45626208333, 12.1651964}, constellationPoint{284.37363625, 18.6647091}, constellationPoint{286.37549375, 18.6882229}, constellationPoint{286.4054775, 16.3550682}, constellationPoint{298.92116541667, 16.4957294}, constellationPoint{298.926, 16.0790844}, constellationPoint{303.5589975, 16.1275158}, constellationPoint{303.636675, 8.8779116}, constellationPoint{306.01395625, 8.9018240}, constellationPoint{306.07910125, 2.4021468}, constellationPoint{309.5798775, 2.4360874}, constellationPoint{309.59884625, 0.4361772}, constellationPoint{309.68464791667, -8.5634165}, constellationPoint{301.69369625, -8.6430750}, constellationPoint{301.72636958333, -11.6762342}, constellationPoint{284.74405375, -11.8664360}, constellationPoint{284.64729291667, -3.8336766}, constellationPoint{280.3981875, -3.8842230}}
constellationPolygons["ARA"] = []constellationPoint{constellationPoint{249.03468125, -60.2644577}, constellationPoint{248.57062375, -45.7670517}, constellationPoint{269.80928375, -45.5163460}, constellationPoint{272.3090175, -45.4859734}, constellationPoint{272.67225291667, -56.9837723}, constellationPoint{265.16818958333, -57.0747757}, constellationPoint{265.77572875, -67.5711060}, constellationPoint{258.2424825, -67.6610870}, constellationPoint{255.72498291667, -67.6905823}, constellationPoint{255.5423925, -65.1916428}, constellationPoint{254.28351458333, -65.2062531}, constellationPoint{254.1951375, -63.7900925}, constellationPoint{251.67632125, -63.8189964}, constellationPoint{251.53784708333, -61.2364578}, constellationPoint{249.08163, -61.2641945}}
constellationPolygons["ARI"] = []constellationPoint{constellationPoint{31.6652475, 10.5143948}, constellationPoint{26.65573375, 10.5432396}, constellationPoint{26.744674583333, 25.6263351}, constellationPoint{30.51371125, 25.6050701}, constellationPoint{30.53061625, 27.8550186}, constellationPoint{38.07014875, 27.8047638}, constellationPoint{38.10319375, 31.2213154}, constellationPoint{42.62838, 31.1865025}, constellationPoint{52.426667916667, 31.1003609}, constellationPoint{52.2906225, 19.4343338}, constellationPoint{51.037234583333, 19.4461136}, constellationPoint{50.94641125, 10.3632069}}
constellationPolygons["AUR"] = []constellationPoint{constellationPoint{69.4869375, 30.9218750}, constellationPoint{69.57384125, 36.2547150}, constellationPoint{72.45734375, 36.2218513}, constellationPoint{72.840285, 52.7196465}, constellationPoint{77.484762083333, 52.6655540}, constellationPoint{77.606764583333, 56.1648331}, constellationPoint{94.13108875, 55.9658089}, constellationPoint{94.05736625, 53.9662552}, constellationPoint{100.04603125, 53.8938293}, constellationPoint{99.919459583333, 49.8945885}, constellationPoint{104.40635875, 49.8410034}, constellationPoint{104.26530291667, 44.3418388}, constellationPoint{112.73412458333, 44.2435493}, constellationPoint{112.56071875, 35.2445297}, constellationPoint{100.09027625, 35.3905640}, constellationPoint{99.965657916667, 27.8913116}, constellationPoint{90.22107125, 28.0092907}, constellationPoint{90.228902916667, 28.5092430}, constellationPoint{73.212475416667, 28.7124405}, constellationPoint{73.235342916667, 30.2123089}, constellationPoint{69.47678875, 30.2552605}}
constellationPolygons["BOO"] = []constellationPoint{constellationPoint{227.78148625, 7.5253930}, constellationPoint{204.06384875, 7.3605771}, constellationPoint{204.02893041667, 14.3604937}, constellationPoint{203.95387208333, 27.8603134}, constellationPoint{210.7888275, 27.8976517}, constellationPoint{210.77085875, 30.1475964}, constellationPoint{211.88893375, 30.1545391}, constellationPoint{211.69873208333, 47.9039383}, constellationPoint{211.58439125, 54.9035759}, constellationPoint{217.25124875, 54.9422379}, constellationPoint{229.59105458333, 55.0448647}, constellationPoint{229.65737375, 52.5451736}, constellationPoint{237.08447375, 52.6174774}, constellationPoint{237.12458541667, 51.1176796}, constellationPoint{237.36542708333, 39.6189079}, constellationPoint{232.64365208333, 39.5721130}, constellationPoint{232.74697791667, 32.5726128}, constellationPoint{229.01591875, 32.5376778}, constellationPoint{229.09951625, 25.5380573}, constellationPoint{227.60549125, 25.5246105}}
constellationPolygons["CAE"] = []constellationPoint{constellationPoint{65.0764125, -39.7007294}, constellationPoint{64.88238625, -48.6996651}, constellationPoint{68.362247916667, -48.7384491}, constellationPoint{68.42409625, -46.2387962}, constellationPoint{73.402082916667, -46.2959023}, constellationPoint{73.482370416667, -42.7963676}, constellationPoint{75.97444375, -42.8255501}, constellationPoint{76.2549375, -27.0772038}, constellationPoint{73.75929625, -27.0479794}, constellationPoint{71.76333125, -27.0248775}, constellationPoint{71.72241875, -29.7746429}, constellationPoint{69.97665125, -29.7546597}, constellationPoint{69.862375416667, -36.7540054}, constellationPoint{65.12985, -36.7010231}}
constellationPolygons["CAM"] = []constellationPoint{constellationPoint{94.13108875, 55.9658089}, constellationPoint{77.606764583333, 56.1648331}, constellationPoint{77.484762083333, 52.6655540}, constellationPoint{72.840285, 52.7196465}, constellationPoint{52.31308625, 52.9366074}, constellationPoint{52.381900416667, 55.4362831}, constellationPoint{49.8540225, 55.4596519}, constellationPoint{49.91349625, 57.4593849}, constellationPoint{48.90093, 57.4684982}, constellationPoint{49.3954575, 68.4662857}, constellationPoint{54.237034583333, 68.4214401}, constellationPoint{55.30874875, 77.4163132}, constellationPoint{56.726209583333, 77.4025955}, constellationPoint{57.53049, 80.3986664}, constellationPoint{80.488894583333, 80.1478500}, constellationPoint{84.536117916667, 85.1239471}, constellationPoint{127.953615, 84.6103745}, constellationPoint{130.40275041667, 86.0975418}, constellationPoint{213.0229575, 85.9308090}, constellationPoint{216.78285625, 79.4449844}, constellationPoint{203.80918958333, 79.3629303}, constellationPoint{204.15701875, 76.3638153}, constellationPoint{195.8206125, 76.3289108}, constellationPoint{174.43479625, 76.3084106}, constellationPoint{174.53158375, 79.3083420}, constellationPoint{162.81859791667, 79.3401794}, constellationPoint{163.10541625, 81.3396072}, constellationPoint{142.191195, 81.4677658}, constellationPoint{140.61547375, 72.9741364}, constellationPoint{123.08622875, 73.1383743}, constellationPoint{122.12910125, 59.6433983}, constellationPoint{107.7531975, 59.8037262}, constellationPoint{107.8515525, 61.8031464}, constellationPoint{94.40745625, 61.9641266}}
constellationPolygons["CNC"] = []constellationPoint{constellationPoint{140.40425875, 6.4700689}, constellationPoint{122.92139125, 6.6302376}, constellationPoint{120.54834291667, 6.6549850}, constellationPoint{120.5806725, 9.6548138}, constellationPoint{118.83248875, 9.6734257}, constellationPoint{118.87160541667, 13.1732168}, constellationPoint{118.94754458333, 19.6728077}, constellationPoint{120.07012375, 19.6608200}, constellationPoint{120.17164625, 27.6602821}, constellationPoint{121.91596958333, 27.6419144}, constellationPoint{121.99323125, 33.1415138}, constellationPoint{140.645985, 32.9691162}}
constellationPolygons["CVN"] = []constellationPoint{constellationPoint{181.59450625, 33.3039627}, constellationPoint{181.59141625, 44.3039627}, constellationPoint{182.82643375, 44.3043365}, constellationPoint{182.8185225, 52.3043365}, constellationPoint{203.74239875, 52.3598061}, constellationPoint{203.79511375, 47.8599281}, constellationPoint{211.69873208333, 47.9039383}, constellationPoint{211.88893375, 30.1545391}, constellationPoint{210.77085875, 30.1475964}, constellationPoint{210.7888275, 27.8976517}, constellationPoint{203.95387208333, 27.8603134}, constellationPoint{200.22657458333, 27.8437748}, constellationPoint{200.20774791667, 31.3437366}, constellationPoint{186.55769375, 31.3074341}, constellationPoint{186.55426041667, 33.3074303}}
constellationPolygons["CMA"] = []constellationPoint{constellationPoint{93.215625, -11.0301533}, constellationPoint{111.97339958333, -11.2521448}, constellationPoint{111.67719875, -33.2504692}, constellationPoint{99.903859583333, -33.1128159}, constellationPoint{92.899067916667, -33.0282326}, constellationPoint{92.99256625, -27.2787991}}
constellationPolygons["CMI"] = []constellationPoint{constellationPoint{122.84900708333, -0.3693900}, constellationPoint{109.59966625, -0.2243290}, constellationPoint{109.61691875, 1.2755718}, constellationPoint{106.86739625, 1.3074419}, constellationPoint{106.91427458333, 5.3071680}, constellationPoint{106.66432208333, 5.3100886}, constellationPoint{106.71787958333, 9.8097754}, constellationPoint{106.7482275, 12.3095980}, constellationPoint{114.24100375, 12.2238722}, constellationPoint{114.2527275, 13.2238064}, constellationPoint{118.87160541667, 13.1732168}, constellationPoint{118.83248875, 9.6734257}, constellationPoint{120.5806725, 9.6548138}, constellationPoint{120.54834291667, 6.6549850}, constellationPoint{122.92139125, 6.6302376}}
constellationPolygons["CAP"] = []constellationPoint{constellationPoint{309.68464791667, -8.5634165}, constellationPoint{301.69369625, -8.6430750}, constellationPoint{301.72636958333, -11.6762342}, constellationPoint{301.91596958333, -27.6419144}, constellationPoint{306.89795541667, -27.5913391}, constellationPoint{321.83163625, -27.4596672}, constellationPoint{321.80777541667, -24.9597607}, constellationPoint{329.77028875, -24.9040413}, constellationPoint{329.6561625, -8.4043999}, constellationPoint{321.668415, -8.4602947}, constellationPoint{321.71645125, -14.4601107}, constellationPoint{309.74390125, -14.5631361}}
constellationPolygons["CAR"] = []constellationPoint{constellationPoint{170.15592125, -57.1843452}, constellationPoint{166.33725625, -57.1744423}, constellationPoint{133.32365541667, -56.9739723}, constellationPoint{133.38017375, -54.9742203}, constellationPoint{127.56711875, -54.9204712}, constellationPoint{127.60929125, -53.4206772}, constellationPoint{123.32011625, -53.3782196}, constellationPoint{123.38112875, -51.1285286}, constellationPoint{120.8616975, -51.1025848}, constellationPoint{90.748902083333, -50.7545471}, constellationPoint{90.693705, -52.5042114}, constellationPoint{93.19435375, -52.5345764}, constellationPoint{93.1074, -55.0340500}, constellationPoint{98.114275416667, -55.0945587}, constellationPoint{97.995077916667, -58.0938416}, constellationPoint{103.01111708333, -58.1537018}, constellationPoint{102.70331375, -64.1518784}, constellationPoint{136.09472708333, -64.4990387}, constellationPoint{135.24368708333, -75.4954681}, constellationPoint{169.85697291667, -75.6840134}, constellationPoint{170.08481125, -64.6842651}}
constellationPolygons["CAS"] = []constellationPoint{constellationPoint{344.34285125, 53.1680298}, constellationPoint{344.30402708333, 56.9179611}, constellationPoint{344.26912375, 59.7512321}, constellationPoint{348.85966375, 59.7646751}, constellationPoint{348.81649041667, 63.6812897}, constellationPoint{355.21757125, 63.6928787}, constellationPoint{355.19785958333, 66.6928711}, constellationPoint{6.76376375, 66.6924438}, constellationPoint{6.92291375, 77.6923447}, constellationPoint{55.30874875, 77.4163132}, constellationPoint{54.237034583333, 68.4214401}, constellationPoint{49.3954575, 68.4662857}, constellationPoint{48.90093, 57.4684982}, constellationPoint{38.762337083333, 57.5513000}, constellationPoint{38.802355416667, 59.0511551}, constellationPoint{30.795625416667, 59.1046104}, constellationPoint{30.77362375, 58.1046753}, constellationPoint{27.5952225, 58.1227188}, constellationPoint{27.53364125, 54.6228828}, constellationPoint{22.45601375, 54.6477699}, constellationPoint{22.40793625, 50.6478767}, constellationPoint{18.60590375, 50.6632347}, constellationPoint{18.588407916667, 48.6632690}, constellationPoint{14.7888675, 48.6757393}, constellationPoint{14.776077083333, 46.6757545}, constellationPoint{4.14327875, 46.6949348}, constellationPoint{4.1463675, 48.6949348}, constellationPoint{355.27607875, 48.6929169}, constellationPoint{355.27055708333, 50.6929131}, constellationPoint{351.4656825, 50.6870193}, constellationPoint{351.45289375, 53.1870041}}
constellationPolygons["CEN"] = []constellationPoint{constellationPoint{166.47936291667, -35.6746559}, constellationPoint{166.45650458333, -40.4246216}, constellationPoint{166.33725625, -57.1744423}, constellationPoint{170.15592125, -57.1843452}, constellationPoint{170.08481125, -64.6842651}, constellationPoint{179.05736375, -64.6957855}, constellationPoint{179.07076791667, -55.6957932}, constellationPoint{194.33451125, -55.6771049}, constellationPoint{194.43838041667, -64.6769638}, constellationPoint{204.68028625, -64.6379395}, constellationPoint{220.51497458333, -64.5390244}, constellationPoint{220.23446541667, -55.5400887}, constellationPoint{214.65681625, -55.5799522}, constellationPoint{214.45026458333, -42.5806465}, constellationPoint{225.79627958333, -42.4941750}, constellationPoint{225.63076958333, -29.9948788}, constellationPoint{190.41739958333, -30.1863899}, constellationPoint{190.42719875, -33.6863785}, constellationPoint{185.38743458333, -33.6938934}, constellationPoint{185.39029625, -35.6938896}}
constellationPolygons["CEP"] = []constellationPoint{constellationPoint{300.5732625, 59.8510780}, constellationPoint{300.48520041667, 61.8506203}, constellationPoint{306.8118675, 61.9143791}, constellationPoint{306.51738125, 67.4129562}, constellationPoint{310.33401458333, 67.4490280}, constellationPoint{309.57304041667, 75.4455261}, constellationPoint{301.87339791667, 75.3708725}, constellationPoint{300.6738, 80.3647766}, constellationPoint{313.70587375, 80.4867859}, constellationPoint{308.72097, 86.4656219}, constellationPoint{308.33135541667, 86.6306305}, constellationPoint{343.51066625, 86.8368912}, constellationPoint{339.26098791667, 88.6638870}, constellationPoint{135.83247125, 87.5689163}, constellationPoint{130.40275041667, 86.0975418}, constellationPoint{127.953615, 84.6103745}, constellationPoint{84.536117916667, 85.1239471}, constellationPoint{80.488894583333, 80.1478500}, constellationPoint{57.53049, 80.3986664}, constellationPoint{56.726209583333, 77.4025955}, constellationPoint{55.30874875, 77.4163132}, constellationPoint{6.92291375, 77.6923447}, constellationPoint{6.76376375, 66.6924438}, constellationPoint{355.19785958333, 66.6928711}, constellationPoint{355.21757125, 63.6928787}, constellationPoint{348.81649041667, 63.6812897}, constellationPoint{348.85966375, 59.7646751}, constellationPoint{344.26912375, 59.7512321}, constellationPoint{344.30402708333, 56.9179611}, constellationPoint{335.91093, 56.8825760}, constellationPoint{335.93130125, 55.6326256}, constellationPoint{333.13762625, 55.6178436}, constellationPoint{333.17467625, 53.3679428}, constellationPoint{330.63921, 53.3532715}, constellationPoint{330.60218875, 55.4364891}, constellationPoint{309.83136125, 55.2753258}, constellationPoint{309.62379458333, 61.3576965}, constellationPoint{308.66080375, 61.3486443}, constellationPoint{308.71659291667, 59.9322395}}
constellationPolygons["CET"] = []constellationPoint{constellationPoint{6.60132875, 0.6925398}, constellationPoint{6.6037875, 2.6925383}, constellationPoint{31.61526625, 2.5978806}, constellationPoint{31.6652475, 10.5143948}, constellationPoint{50.94641125, 10.3632069}, constellationPoint{50.85298375, 0.4469725}, constellationPoint{50.836682916667, -1.3029516}, constellationPoint{41.33922125, -1.2210265}, constellationPoint{41.14875875, -23.8536034}, constellationPoint{26.46599875, -23.7562580}, constellationPoint{26.45888875, -24.8729095}, constellationPoint{359.11056458333, -24.8042011}, constellationPoint{359.10329875, -6.3042021}, constellationPoint{6.5927025, -6.3074551}}
constellationPolygons["CHA"] = []constellationPoint{constellationPoint{111.65211458333, -82.7758865}, constellationPoint{209.11110875, -83.1200714}, constellationPoint{207.78143375, -75.6235962}, constellationPoint{169.85697291667, -75.6840134}, constellationPoint{135.24368708333, -75.4954681}, constellationPoint{114.21470375, -75.2899170}}
constellationPolygons["CIR"] = []constellationPoint{constellationPoint{204.68028625, -64.6379395}, constellationPoint{204.70747958333, -65.6378784}, constellationPoint{207.26802291667, -65.6249542}, constellationPoint{207.46087041667, -70.6244431}, constellationPoint{224.16644125, -70.5115433}, constellationPoint{224.00363375, -68.0122070}, constellationPoint{226.55712625, -67.9909286}, constellationPoint{226.35353541667, -64.0751266}, constellationPoint{230.16657875, -64.0415649}, constellationPoint{230.05456958333, -61.4587479}, constellationPoint{232.58976458333, -61.4353065}, constellationPoint{232.5498675, -60.4354935}, constellationPoint{232.38191125, -55.4362831}, constellationPoint{228.08351125, -55.4754944}, constellationPoint{220.23446541667, -55.5400887}, constellationPoint{220.51497458333, -64.5390244}}
constellationPolygons["COL"] = []constellationPoint{constellationPoint{75.97444375, -42.8255501}, constellationPoint{76.2549375, -27.0772038}, constellationPoint{92.99256625, -27.2787991}, constellationPoint{92.899067916667, -33.0282326}, constellationPoint{99.903859583333, -33.1128159}, constellationPoint{99.70891625, -43.1116486}, constellationPoint{90.951777083333, -43.0057793}}
constellationPolygons["COM"] = []constellationPoint{constellationPoint{179.60453541667, 13.3040485}, constellationPoint{179.60894125, 28.3040466}, constellationPoint{181.59566375, 28.3039627}, constellationPoint{181.59450625, 33.3039627}, constellationPoint{186.55426041667, 33.3074303}, constellationPoint{186.55769375, 31.3074341}, constellationPoint{200.20774791667, 31.3437366}, constellationPoint{200.22657458333, 27.8437748}, constellationPoint{203.95387208333, 27.8603134}, constellationPoint{204.02893041667, 14.3604937}, constellationPoint{194.05906625, 14.3225088}, constellationPoint{194.0620275, 13.3225126}}
constellationPolygons["CRA"] = []constellationPoint{constellationPoint{269.62546375, -37.0174599}, constellationPoint{289.59631958333, -36.7785645}, constellationPoint{289.76964, -45.2775650}, constellationPoint{272.3090175, -45.4859734}, constellationPoint{269.80928375, -45.5163460}}
constellationPolygons["CRB"] = []constellationPoint{constellationPoint{229.09951625, 25.5380573}, constellationPoint{229.01591875, 32.5376778}, constellationPoint{232.74697791667, 32.5726128}, constellationPoint{232.64365208333, 39.5721130}, constellationPoint{237.36542708333, 39.6189079}, constellationPoint{246.07194875, 39.7117195}, constellationPoint{246.2798025, 26.7128716}, constellationPoint{243.78670625, 26.6855240}, constellationPoint{243.8001825, 25.6855946}, constellationPoint{241.80573458333, 25.6641407}}
constellationPolygons["CRV"] = []constellationPoint{constellationPoint{194.1330525, -11.6773882}, constellationPoint{179.09676, -11.6957970}, constellationPoint{179.09131041667, -25.1957951}, constellationPoint{190.404525, -25.1864014}, constellationPoint{190.3985025, -22.6864090}, constellationPoint{194.16687, -22.6773415}}
constellationPolygons["CRT"] = []constellationPoint{constellationPoint{162.82713875, -6.6621790}, constellationPoint{162.80791375, -11.6621428}, constellationPoint{162.77554041667, -19.6620827}, constellationPoint{164.03058625, -19.6666222}, constellationPoint{164.00808291667, -25.1665821}, constellationPoint{179.09131041667, -25.1957951}, constellationPoint{179.09676, -11.6957970}, constellationPoint{179.09860625, -6.6957974}, constellationPoint{174.34229875, -6.6916924}}
constellationPolygons["CRU"] = []constellationPoint{constellationPoint{179.07076791667, -55.6957932}, constellationPoint{179.05736375, -64.6957855}, constellationPoint{194.43838041667, -64.6769638}, constellationPoint{194.33451125, -55.6771049}}
constellationPolygons["CYG"] = []constellationPoint{constellationPoint{290.13264625, 27.7324085}, constellationPoint{290.0952525, 30.2321968}, constellationPoint{291.5987775, 30.2493153}, constellationPoint{291.49260458333, 36.7487144}, constellationPoint{292.11965541667, 36.7558022}, constellationPoint{291.9835275, 43.7550354}, constellationPoint{288.47030708333, 43.7149391}, constellationPoint{288.37552041667, 47.7143936}, constellationPoint{287.1206475, 47.6998672}, constellationPoint{286.87645958333, 55.6984482}, constellationPoint{291.90459291667, 55.7560043}, constellationPoint{291.80955, 58.2554703}, constellationPoint{297.10055375, 58.3138733}, constellationPoint{297.03924125, 59.8135414}, constellationPoint{300.5732625, 59.8510780}, constellationPoint{308.71659291667, 59.9322395}, constellationPoint{308.66080375, 61.3486443}, constellationPoint{309.62379458333, 61.3576965}, constellationPoint{309.83136125, 55.2753258}, constellationPoint{330.60218875, 55.4364891}, constellationPoint{330.63921, 53.3532715}, constellationPoint{330.76266291667, 44.6036453}, constellationPoint{329.87860625, 44.5982513}, constellationPoint{329.88163958333, 44.3482628}, constellationPoint{329.37664041667, 44.3451195}, constellationPoint{329.4610125, 36.5953827}, constellationPoint{327.319965, 36.5815468}, constellationPoint{327.39518125, 28.5817947}, constellationPoint{322.62016375, 28.5480537}, constellationPoint{315.08391458333, 28.4871883}, constellationPoint{315.07258375, 29.4871387}, constellationPoint{296.25094375, 29.3010578}, constellationPoint{296.27220125, 27.8011742}}
constellationPolygons["DEL"] = []constellationPoint{constellationPoint{309.5798775, 2.4360874}, constellationPoint{306.07910125, 2.4021468}, constellationPoint{306.01395625, 8.9018240}, constellationPoint{303.636675, 8.8779116}, constellationPoint{303.5589975, 16.1275158}, constellationPoint{305.18694875, 16.1439629}, constellationPoint{305.13404875, 20.8936996}, constellationPoint{309.89693708333, 20.9399471}, constellationPoint{309.907665, 19.9399967}, constellationPoint{317.17878291667, 20.0046406}, constellationPoint{317.24836375, 12.3382607}, constellationPoint{314.61859625, 12.3157644}, constellationPoint{314.67109625, 6.4826641}, constellationPoint{314.045505, 6.4771614}, constellationPoint{314.08109708333, 2.4773185}}
constellationPolygons["DOR"] = []constellationPoint{constellationPoint{58.318787916667, -52.7968445}, constellationPoint{60.79789625, -52.8228111}, constellationPoint{60.69291875, -56.1555862}, constellationPoint{65.6504625, -56.2093849}, constellationPoint{65.55459, -58.7088547}, constellationPoint{69.274534583333, -58.7506638}, constellationPoint{68.79401875, -67.2479248}, constellationPoint{68.58152375, -69.7467194}, constellationPoint{98.454422916667, -70.1041336}, constellationPoint{98.93724875, -64.1070251}, constellationPoint{90.173642916667, -64.0010529}, constellationPoint{90.34506125, -61.0020981}, constellationPoint{82.85761375, -60.9112892}, constellationPoint{83.01880375, -57.4122620}, constellationPoint{75.547742916667, -57.3230400}, constellationPoint{75.6770175, -53.8238029}, constellationPoint{68.217745416667, -53.7376366}, constellationPoint{68.362247916667, -48.7384491}, constellationPoint{64.88238625, -48.6996651}, constellationPoint{62.149907916667, -48.6699715}, constellationPoint{62.098639583333, -50.6697006}, constellationPoint{58.37716625, -50.6304779}}
constellationPolygons["DRA"] = []constellationPoint{constellationPoint{140.61547375, 72.9741364}, constellationPoint{142.191195, 81.4677658}, constellationPoint{163.10541625, 81.3396072}, constellationPoint{162.81859791667, 79.3401794}, constellationPoint{174.53158375, 79.3083420}, constellationPoint{174.43479625, 76.3084106}, constellationPoint{195.8206125, 76.3289108}, constellationPoint{196.09747375, 69.3293610}, constellationPoint{210.65081125, 69.3991165}, constellationPoint{210.82055541667, 65.3996506}, constellationPoint{235.32956541667, 65.6023483}, constellationPoint{235.05063, 69.6009445}, constellationPoint{247.8410625, 69.7383041}, constellationPoint{247.2207075, 74.7347870}, constellationPoint{261.53663708333, 74.9033127}, constellationPoint{260.21790458333, 79.8953476}, constellationPoint{267.65602041667, 79.9857483}, constellationPoint{261.72223041667, 85.9495697}, constellationPoint{308.72097, 86.4656219}, constellationPoint{313.70587375, 80.4867859}, constellationPoint{300.6738, 80.3647766}, constellationPoint{301.87339791667, 75.3708725}, constellationPoint{309.57304041667, 75.4455261}, constellationPoint{310.33401458333, 67.4490280}, constellationPoint{306.51738125, 67.4129562}, constellationPoint{306.8118675, 61.9143791}, constellationPoint{300.48520041667, 61.8506203}, constellationPoint{300.5732625, 59.8510780}, constellationPoint{297.03924125, 59.8135414}, constellationPoint{297.10055375, 58.3138733}, constellationPoint{291.80955, 58.2554703}, constellationPoint{291.90459291667, 55.7560043}, constellationPoint{286.87645958333, 55.6984482}, constellationPoint{287.1206475, 47.6998672}, constellationPoint{274.34237541667, 47.5476036}, constellationPoint{274.25768875, 50.5470886}, constellationPoint{255.7863525, 50.3244438}, constellationPoint{255.75682625, 51.3242683}, constellationPoint{237.12458541667, 51.1176796}, constellationPoint{237.08447375, 52.6174774}, constellationPoint{229.65737375, 52.5451736}, constellationPoint{229.59105458333, 55.0448647}, constellationPoint{217.25124875, 54.9422379}, constellationPoint{217.04525375, 62.4414825}, constellationPoint{203.57364125, 62.3593979}, constellationPoint{203.55053875, 63.3593445}, constellationPoint{181.58155958333, 63.3039627}, constellationPoint{181.57925541667, 65.8039627}, constellationPoint{171.84934625, 65.8126068}, constellationPoint{171.96136958333, 72.8125000}}
constellationPolygons["EQU"] = []constellationPoint{constellationPoint{314.08109708333, 2.4773185}, constellationPoint{314.045505, 6.4771614}, constellationPoint{314.67109625, 6.4826641}, constellationPoint{314.61859625, 12.3157644}, constellationPoint{317.24836375, 12.3382607}, constellationPoint{318.25026458333, 12.3465548}, constellationPoint{318.244515, 13.0132008}, constellationPoint{321.50110208333, 13.0390635}, constellationPoint{321.58347125, 2.5393796}}
constellationPolygons["ERI"] = []constellationPoint{constellationPoint{55.352905416667, 0.4037257}, constellationPoint{70.852360416667, 0.2375014}, constellationPoint{71.60231375, 0.2289162}, constellationPoint{71.55635875, -3.7708201}, constellationPoint{77.804395416667, -3.8437285}, constellationPoint{77.72003125, -10.8432293}, constellationPoint{75.22175125, -10.8138046}, constellationPoint{75.178714583333, -14.3135529}, constellationPoint{73.929797916667, -14.2989721}, constellationPoint{73.75929625, -27.0479794}, constellationPoint{71.76333125, -27.0248775}, constellationPoint{71.72241875, -29.7746429}, constellationPoint{69.97665125, -29.7546597}, constellationPoint{69.862375416667, -36.7540054}, constellationPoint{65.12985, -36.7010231}, constellationPoint{65.0764125, -39.7007294}, constellationPoint{59.105884583333, -39.6368256}, constellationPoint{59.031364583333, -43.6364403}, constellationPoint{52.3267725, -43.5694046}, constellationPoint{52.2890025, -45.5692215}, constellationPoint{46.09077125, -45.5124779}, constellationPoint{46.03451375, -48.5122337}, constellationPoint{41.08530875, -48.4710045}, constellationPoint{41.04769375, -50.4708595}, constellationPoint{37.34121, -50.4425697}, constellationPoint{37.28334625, -53.4423561}, constellationPoint{33.58414375, -53.4164696}, constellationPoint{33.489424583333, -57.9161568}, constellationPoint{21.20622875, -57.8484154}, constellationPoint{21.2732625, -52.8485603}, constellationPoint{24.967354583333, -52.8658562}, constellationPoint{24.9938475, -50.8659210}, constellationPoint{28.693257083333, -50.8859215}, constellationPoint{28.738322916667, -47.5527229}, constellationPoint{36.1529475, -47.6004944}, constellationPoint{36.26401375, -39.4342155}, constellationPoint{46.187260416667, -39.5128975}, constellationPoint{46.193322083333, -39.0962563}, constellationPoint{53.64428375, -39.1650963}, constellationPoint{53.699605416667, -35.5820351}, constellationPoint{57.430512083333, -35.6192436}, constellationPoint{57.58890125, -24.0033779}, constellationPoint{41.14875875, -23.8536034}, constellationPoint{41.33922125, -1.2210265}, constellationPoint{50.836682916667, -1.3029516}, constellationPoint{55.335582083333, -1.3461887}}
constellationPolygons["FOR"] = []constellationPoint{constellationPoint{26.46599875, -23.7562580}, constellationPoint{41.14875875, -23.8536034}, constellationPoint{57.58890125, -24.0033779}, constellationPoint{57.430512083333, -35.6192436}, constellationPoint{53.699605416667, -35.5820351}, constellationPoint{53.64428375, -39.1650963}, constellationPoint{46.193322083333, -39.0962563}, constellationPoint{46.187260416667, -39.5128975}, constellationPoint{36.26401375, -39.4342155}, constellationPoint{26.350727916667, -39.3726234}, constellationPoint{26.45888875, -24.8729095}}
constellationPolygons["GEM"] = []constellationPoint{constellationPoint{96.37276375, 11.9332972}, constellationPoint{96.4439175, 17.4328651}, constellationPoint{95.069575416667, 17.4495068}, constellationPoint{95.1241275, 21.4491768}, constellationPoint{90.125155416667, 21.5098724}, constellationPoint{90.1440375, 22.8430862}, constellationPoint{90.22107125, 28.0092907}, constellationPoint{99.965657916667, 27.8913116}, constellationPoint{100.09027625, 35.3905640}, constellationPoint{112.56071875, 35.2445297}, constellationPoint{118.28970875, 35.1810532}, constellationPoint{118.25808, 33.1812286}, constellationPoint{121.99323125, 33.1415138}, constellationPoint{121.91596958333, 27.6419144}, constellationPoint{120.17164625, 27.6602821}, constellationPoint{120.07012375, 19.6608200}, constellationPoint{118.94754458333, 19.6728077}, constellationPoint{118.87160541667, 13.1732168}, constellationPoint{114.2527275, 13.2238064}, constellationPoint{114.24100375, 12.2238722}, constellationPoint{106.7482275, 12.3095980}, constellationPoint{106.71787958333, 9.8097754}, constellationPoint{105.71845958333, 9.8214874}, constellationPoint{105.742815, 11.8213453}}
constellationPolygons["GRU"] = []constellationPoint{constellationPoint{321.92805291667, -36.4592972}, constellationPoint{322.04232125, -44.9588585}, constellationPoint{322.11736625, -49.4585724}, constellationPoint{331.998825, -49.3911743}, constellationPoint{332.113695, -56.3908348}, constellationPoint{351.76852208333, -56.3126869}, constellationPoint{351.69270458333, -39.3127594}, constellationPoint{351.6833775, -36.3127670}, constellationPoint{346.72751375, -36.3249741}}
constellationPolygons["HER"] = []constellationPoint{constellationPoint{245.558595, 3.7033811}, constellationPoint{242.80966791667, 3.6735139}, constellationPoint{242.67663, 15.6728001}, constellationPoint{240.18107375, 15.6463346}, constellationPoint{240.1110075, 21.6459675}, constellationPoint{241.85657541667, 21.6644115}, constellationPoint{241.80573458333, 25.6641407}, constellationPoint{243.8001825, 25.6855946}, constellationPoint{243.78670625, 26.6855240}, constellationPoint{246.2798025, 26.7128716}, constellationPoint{246.07194875, 39.7117195}, constellationPoint{237.36542708333, 39.6189079}, constellationPoint{237.12458541667, 51.1176796}, constellationPoint{255.75682625, 51.3242683}, constellationPoint{255.7863525, 50.3244438}, constellationPoint{274.25768875, 50.5470886}, constellationPoint{274.34237541667, 47.5476036}, constellationPoint{273.46687375, 47.5369873}, constellationPoint{273.82438625, 30.0391560}, constellationPoint{276.70077291667, 30.0739765}, constellationPoint{276.76288625, 26.0743504}, constellationPoint{284.2698675, 26.1640968}, constellationPoint{284.27716208333, 25.6641407}, constellationPoint{284.33913291667, 21.2478352}, constellationPoint{284.37363625, 18.6647091}, constellationPoint{284.45626208333, 12.1651964}, constellationPoint{281.388045, 12.1287737}, constellationPoint{275.20308458333, 12.0543308}, constellationPoint{275.17327375, 14.3874788}, constellationPoint{260.17687791667, 14.2060347}, constellationPoint{260.19584708333, 12.7061481}, constellationPoint{252.7014825, 12.6179380}, constellationPoint{252.80590958333, 3.7852108}}
constellationPolygons["HOR"] = []constellationPoint{constellationPoint{65.0764125, -39.7007294}, constellationPoint{64.88238625, -48.6996651}, constellationPoint{62.149907916667, -48.6699715}, constellationPoint{62.098639583333, -50.6697006}, constellationPoint{58.37716625, -50.6304779}, constellationPoint{58.318787916667, -52.7968445}, constellationPoint{53.365002083333, -52.7470779}, constellationPoint{53.23681625, -57.0797844}, constellationPoint{48.79113125, -57.0377846}, constellationPoint{48.362689583333, -67.0358200}, constellationPoint{33.202360416667, -66.9151917}, constellationPoint{33.489424583333, -57.9161568}, constellationPoint{33.58414375, -53.4164696}, constellationPoint{37.28334625, -53.4423561}, constellationPoint{37.34121, -50.4425697}, constellationPoint{41.04769375, -50.4708595}, constellationPoint{41.08530875, -48.4710045}, constellationPoint{46.03451375, -48.5122337}, constellationPoint{46.09077125, -45.5124779}, constellationPoint{52.2890025, -45.5692215}, constellationPoint{52.3267725, -43.5694046}, constellationPoint{59.031364583333, -43.6364403}, constellationPoint{59.105884583333, -39.6368256}}
constellationPolygons["HYA"] = []constellationPoint{constellationPoint{122.84900708333, -0.3693900}, constellationPoint{122.92139125, 6.6302376}, constellationPoint{140.40425875, 6.4700689}, constellationPoint{145.39841708333, 6.4327669}, constellationPoint{145.34890625, -0.5670585}, constellationPoint{145.27027208333, -11.5667810}, constellationPoint{162.80791375, -11.6621428}, constellationPoint{162.77554041667, -19.6620827}, constellationPoint{164.03058625, -19.6666222}, constellationPoint{164.00808291667, -25.1665821}, constellationPoint{179.09131041667, -25.1957951}, constellationPoint{190.404525, -25.1864014}, constellationPoint{190.3985025, -22.6864090}, constellationPoint{194.16687, -22.6773415}, constellationPoint{215.51309125, -22.5727749}, constellationPoint{215.53369041667, -25.0727024}, constellationPoint{225.57655375, -24.9951096}, constellationPoint{225.63076958333, -29.9948788}, constellationPoint{190.41739958333, -30.1863899}, constellationPoint{190.42719875, -33.6863785}, constellationPoint{185.38743458333, -33.6938934}, constellationPoint{185.39029625, -35.6938896}, constellationPoint{166.47936291667, -35.6746559}, constellationPoint{163.95851541667, -35.6664963}, constellationPoint{163.977885, -31.8332005}, constellationPoint{160.20137875, -31.8185863}, constellationPoint{160.21289375, -29.8186131}, constellationPoint{155.18132708333, -29.7947845}, constellationPoint{155.1993375, -27.1281624}, constellationPoint{147.65928291667, -27.0835037}, constellationPoint{147.67968125, -24.5835705}, constellationPoint{141.904335, -24.5425186}, constellationPoint{137.63677625, -24.5086308}, constellationPoint{137.68497, -19.5088310}, constellationPoint{130.1635125, -19.4423733}, constellationPoint{130.18434, -17.4424706}, constellationPoint{126.92709458333, -17.4112568}, constellationPoint{126.98977958333, -11.4115648}, constellationPoint{122.73417875, -11.3687992}}
constellationPolygons["HYI"] = []constellationPoint{constellationPoint{68.79401875, -67.2479248}, constellationPoint{68.58152375, -69.7467194}, constellationPoint{67.957485, -74.7431641}, constellationPoint{52.075782083333, -74.5741272}, constellationPoint{50.091655416667, -82.0644531}, constellationPoint{1.53339125, -81.8039551}, constellationPoint{1.5662970833333, -74.3039627}, constellationPoint{12.3324375, -74.3185730}, constellationPoint{12.295414583333, -75.3185272}, constellationPoint{20.654050416667, -75.3472214}, constellationPoint{21.20622875, -57.8484154}, constellationPoint{33.489424583333, -57.9161568}, constellationPoint{33.202360416667, -66.9151917}, constellationPoint{48.362689583333, -67.0358200}}
constellationPolygons["IND"] = []constellationPoint{constellationPoint{323.1847575, -74.4544678}, constellationPoint{351.99783291667, -74.3124619}, constellationPoint{351.86139125, -66.8125992}, constellationPoint{332.3985675, -66.8899918}, constellationPoint{332.113695, -56.3908348}, constellationPoint{331.998825, -49.3911743}, constellationPoint{322.11736625, -49.4585724}, constellationPoint{322.04232125, -44.9588585}, constellationPoint{307.169295, -45.0900002}, constellationPoint{307.45880125, -56.5885773}, constellationPoint{307.56480125, -59.5880547}, constellationPoint{322.34865125, -59.4576836}}
constellationPolygons["LAC"] = []constellationPoint{constellationPoint{329.4610125, 36.5953827}, constellationPoint{329.37664041667, 44.3451195}, constellationPoint{329.88163958333, 44.3482628}, constellationPoint{329.87860625, 44.5982513}, constellationPoint{330.76266291667, 44.6036453}, constellationPoint{330.63921, 53.3532715}, constellationPoint{333.17467625, 53.3679428}, constellationPoint{333.13762625, 55.6178436}, constellationPoint{335.93130125, 55.6326256}, constellationPoint{335.91093, 56.8825760}, constellationPoint{344.30402708333, 56.9179611}, constellationPoint{344.34285125, 53.1680298}, constellationPoint{344.46530375, 35.1682358}, constellationPoint{343.70919291667, 35.1656151}, constellationPoint{343.70653208333, 35.6656113}, constellationPoint{331.35955791667, 35.6069336}, constellationPoint{331.35046041667, 36.6069069}}
constellationPolygons["LEO"] = []constellationPoint{constellationPoint{162.8497125, -0.6622211}, constellationPoint{162.87601958333, 6.3377299}, constellationPoint{145.39841708333, 6.4327669}, constellationPoint{140.40425875, 6.4700689}, constellationPoint{140.645985, 32.9691162}, constellationPoint{150.08438541667, 32.9022789}, constellationPoint{150.04234375, 27.9024086}, constellationPoint{159.23840125, 27.8529167}, constellationPoint{159.2108775, 22.8529778}, constellationPoint{162.94253875, 22.8376045}, constellationPoint{162.95149375, 24.8375893}, constellationPoint{166.6809375, 24.8250446}, constellationPoint{166.69399791667, 28.3250256}, constellationPoint{179.60894125, 28.3040466}, constellationPoint{179.60453541667, 13.3040485}, constellationPoint{179.60373458333, 10.3040485}, constellationPoint{174.36568791667, 10.3082914}, constellationPoint{174.35052458333, -0.6916979}, constellationPoint{174.34229875, -6.6916924}, constellationPoint{162.82713875, -6.6621790}}
constellationPolygons["LMI"] = []constellationPoint{constellationPoint{140.645985, 32.9691162}, constellationPoint{140.72163125, 39.2188187}, constellationPoint{145.6819725, 39.1817665}, constellationPoint{145.70923791667, 41.4316750}, constellationPoint{154.37822375, 41.3773613}, constellationPoint{154.3594125, 39.3774109}, constellationPoint{163.52316875, 39.3356133}, constellationPoint{163.48940875, 33.3356781}, constellationPoint{166.71422541667, 33.3249931}, constellationPoint{166.69399791667, 28.3250256}, constellationPoint{166.6809375, 24.8250446}, constellationPoint{162.95149375, 24.8375893}, constellationPoint{162.94253875, 22.8376045}, constellationPoint{159.2108775, 22.8529778}, constellationPoint{159.23840125, 27.8529167}, constellationPoint{150.04234375, 27.9024086}, constellationPoint{150.08438541667, 32.9022789}}
constellationPolygons["LEP"] = []constellationPoint{constellationPoint{73.75929625, -27.0479794}, constellationPoint{76.2549375, -27.0772038}, constellationPoint{92.99256625, -27.2787991}, constellationPoint{93.215625, -11.0301533}, constellationPoint{88.965790416667, -10.9785318}, constellationPoint{77.72003125, -10.8432293}, constellationPoint{75.22175125, -10.8138046}, constellationPoint{75.178714583333, -14.3135529}, constellationPoint{73.929797916667, -14.2989721}}
constellationPolygons["LIB"] = []constellationPoint{constellationPoint{227.85301208333, -0.4742887}, constellationPoint{221.6030925, -0.5269387}, constellationPoint{221.66710791667, -8.5266848}, constellationPoint{215.40850625, -8.5731344}, constellationPoint{215.51309125, -22.5727749}, constellationPoint{215.53369041667, -25.0727024}, constellationPoint{225.57655375, -24.9951096}, constellationPoint{225.63076958333, -29.9948788}, constellationPoint{236.92998, -29.8896160}, constellationPoint{236.81306375, -20.3902016}, constellationPoint{240.57177625, -20.3516178}, constellationPoint{240.43727875, -8.3523235}, constellationPoint{240.38695375, -3.6025870}, constellationPoint{227.88195125, -3.7241600}}
constellationPolygons["LUP"] = []constellationPoint{constellationPoint{214.65681625, -55.5799522}, constellationPoint{220.23446541667, -55.5400887}, constellationPoint{228.08351125, -55.4754944}, constellationPoint{228.05671625, -54.4756165}, constellationPoint{232.35337208333, -54.4364166}, constellationPoint{232.20724625, -48.4371071}, constellationPoint{237.24728125, -48.3880234}, constellationPoint{237.12458541667, -42.3886375}, constellationPoint{242.15280625, -42.3366776}, constellationPoint{241.94769875, -29.8377628}, constellationPoint{236.92998, -29.8896160}, constellationPoint{225.63076958333, -29.9948788}, constellationPoint{225.79627958333, -42.4941750}, constellationPoint{214.45026458333, -42.5806465}}
constellationPolygons["LYN"] = []constellationPoint{constellationPoint{112.56071875, 35.2445297}, constellationPoint{112.73412458333, 44.2435493}, constellationPoint{104.26530291667, 44.3418388}, constellationPoint{104.40635875, 49.8410034}, constellationPoint{99.919459583333, 49.8945885}, constellationPoint{100.04603125, 53.8938293}, constellationPoint{94.05736625, 53.9662552}, constellationPoint{94.13108875, 55.9658089}, constellationPoint{94.40745625, 61.9641266}, constellationPoint{107.8515525, 61.8031464}, constellationPoint{107.7531975, 59.8037262}, constellationPoint{122.12910125, 59.6433983}, constellationPoint{128.79913375, 59.5759888}, constellationPoint{128.44010375, 46.5777283}, constellationPoint{139.59071125, 46.4782791}, constellationPoint{139.51249041667, 41.4785957}, constellationPoint{145.70923791667, 41.4316750}, constellationPoint{145.6819725, 39.1817665}, constellationPoint{140.72163125, 39.2188187}, constellationPoint{140.645985, 32.9691162}, constellationPoint{121.99323125, 33.1415138}, constellationPoint{118.25808, 33.1812286}, constellationPoint{118.28970875, 35.1810532}}
constellationPolygons["LYR"] = []constellationPoint{constellationPoint{284.27716208333, 25.6641407}, constellationPoint{284.2698675, 26.1640968}, constellationPoint{276.76288625, 26.0743504}, constellationPoint{276.70077291667, 30.0739765}, constellationPoint{273.82438625, 30.0391560}, constellationPoint{273.46687375, 47.5369873}, constellationPoint{274.34237541667, 47.5476036}, constellationPoint{287.1206475, 47.6998672}, constellationPoint{288.37552041667, 47.7143936}, constellationPoint{288.47030708333, 43.7149391}, constellationPoint{291.9835275, 43.7550354}, constellationPoint{292.11965541667, 36.7558022}, constellationPoint{291.49260458333, 36.7487144}, constellationPoint{291.5987775, 30.2493153}, constellationPoint{290.0952525, 30.2321968}, constellationPoint{290.13264625, 27.7324085}, constellationPoint{290.16131375, 25.7325745}}
constellationPolygons["MEN"] = []constellationPoint{constellationPoint{109.01970875, -85.2614441}, constellationPoint{48.23292, -84.5553818}, constellationPoint{50.091655416667, -82.0644531}, constellationPoint{52.075782083333, -74.5741272}, constellationPoint{67.957485, -74.7431641}, constellationPoint{68.58152375, -69.7467194}, constellationPoint{98.454422916667, -70.1041336}, constellationPoint{97.770709583333, -75.1000366}, constellationPoint{114.21470375, -75.2899170}, constellationPoint{111.65211458333, -82.7758865}}
constellationPolygons["MIC"] = []constellationPoint{constellationPoint{306.89795541667, -27.5913391}, constellationPoint{321.83163625, -27.4596672}, constellationPoint{321.92805291667, -36.4592972}, constellationPoint{322.04232125, -44.9588585}, constellationPoint{307.169295, -45.0900002}}
constellationPolygons["MON"] = []constellationPoint{constellationPoint{95.225680416667, -0.0537102}, constellationPoint{95.34803125, 9.9455481}, constellationPoint{96.347665416667, 9.9334478}, constellationPoint{96.37276375, 11.9332972}, constellationPoint{105.742815, 11.8213453}, constellationPoint{105.71845958333, 9.8214874}, constellationPoint{106.71787958333, 9.8097754}, constellationPoint{106.66432208333, 5.3100886}, constellationPoint{106.91427458333, 5.3071680}, constellationPoint{106.86739625, 1.3074419}, constellationPoint{109.61691875, 1.2755718}, constellationPoint{109.59966625, -0.2243290}, constellationPoint{122.84900708333, -0.3693900}, constellationPoint{122.73417875, -11.3687992}, constellationPoint{111.97339958333, -11.2521448}, constellationPoint{93.215625, -11.0301533}, constellationPoint{88.965790416667, -10.9785318}, constellationPoint{89.052372083333, -3.9790573}, constellationPoint{95.177142083333, -4.0534163}}
constellationPolygons["MUS"] = []constellationPoint{constellationPoint{170.08481125, -64.6842651}, constellationPoint{169.85697291667, -75.6840134}, constellationPoint{207.78143375, -75.6235962}, constellationPoint{207.46087041667, -70.6244431}, constellationPoint{207.26802291667, -65.6249542}, constellationPoint{204.70747958333, -65.6378784}, constellationPoint{204.68028625, -64.6379395}, constellationPoint{194.43838041667, -64.6769638}, constellationPoint{179.05736375, -64.6957855}}
constellationPolygons["NOR"] = []constellationPoint{constellationPoint{232.5498675, -60.4354935}, constellationPoint{249.03468125, -60.2644577}, constellationPoint{248.57062375, -45.7670517}, constellationPoint{248.4947775, -42.2674789}, constellationPoint{242.15280625, -42.3366776}, constellationPoint{237.12458541667, -42.3886375}, constellationPoint{237.24728125, -48.3880234}, constellationPoint{232.20724625, -48.4371071}, constellationPoint{232.35337208333, -54.4364166}, constellationPoint{228.05671625, -54.4756165}, constellationPoint{228.08351125, -55.4754944}, constellationPoint{232.38191125, -55.4362831}}
constellationPolygons["OCT"] = []constellationPoint{constellationPoint{0.80064625, -89.3039017}, constellationPoint{1.53339125, -81.8039551}, constellationPoint{50.091655416667, -82.0644531}, constellationPoint{48.23292, -84.5553818}, constellationPoint{109.01970875, -85.2614441}, constellationPoint{111.65211458333, -82.7758865}, constellationPoint{209.11110875, -83.1200714}, constellationPoint{276.86599791667, -82.4582748}, constellationPoint{274.19506041667, -74.9745178}, constellationPoint{323.1847575, -74.4544678}, constellationPoint{351.99783291667, -74.3124619}, constellationPoint{1.56630625, -74.3039627}, constellationPoint{0.80064625, -89.3039017}, constellationPoint{0.80065208333333, -89.3038940}}
constellationPolygons["OPH"] = []constellationPoint{constellationPoint{245.6026275, -0.2963768}, constellationPoint{245.558595, 3.7033811}, constellationPoint{252.80590958333, 3.7852108}, constellationPoint{252.7014825, 12.6179380}, constellationPoint{260.19584708333, 12.7061481}, constellationPoint{260.17687791667, 14.2060347}, constellationPoint{275.17327375, 14.3874788}, constellationPoint{275.20308458333, 12.0543308}, constellationPoint{281.388045, 12.1287737}, constellationPoint{281.45856875, 6.3791943}, constellationPoint{275.27461041667, 6.3047633}, constellationPoint{275.29601125, 4.5548930}, constellationPoint{277.87113125, 4.5860157}, constellationPoint{277.88929958333, 3.0861249}, constellationPoint{275.31423625, 3.0550034}, constellationPoint{275.35059875, 0.0552235}, constellationPoint{269.10103791667, -0.0206471}, constellationPoint{269.14970375, -4.0203514}, constellationPoint{271.14967375, -3.9960551}, constellationPoint{271.22371625, -9.9956055}, constellationPoint{266.72375708333, -10.0502338}, constellationPoint{266.7447, -11.7167768}, constellationPoint{265.49440375, -11.7319136}, constellationPoint{265.47349041667, -10.0653696}, constellationPoint{259.22206958333, -10.1404381}, constellationPoint{259.29742791667, -16.1399899}, constellationPoint{265.80019041667, -16.0618820}, constellationPoint{266.00183541667, -30.0606632}, constellationPoint{253.23534875, -30.2123089}, constellationPoint{253.15567041667, -24.7960968}, constellationPoint{245.89144625, -24.8781185}, constellationPoint{245.82298375, -19.5451660}, constellationPoint{247.45067541667, -19.5271549}, constellationPoint{247.43823, -18.5272255}, constellationPoint{245.81068041667, -18.5452347}, constellationPoint{245.69120375, -8.2958899}, constellationPoint{240.43727875, -8.3523235}, constellationPoint{240.38695375, -3.6025870}, constellationPoint{245.63838875, -3.5461800}}
constellationPolygons["ORI"] = []constellationPoint{constellationPoint{70.852360416667, 0.2375014}, constellationPoint{71.03402875, 15.7364635}, constellationPoint{76.28892625, 15.6755352}, constellationPoint{76.29527875, 16.1754990}, constellationPoint{81.7987125, 16.1101055}, constellationPoint{81.7922325, 15.6101446}, constellationPoint{85.79364625, 15.5619202}, constellationPoint{85.755057083333, 12.5621548}, constellationPoint{88.255369583333, 12.5318508}, constellationPoint{88.327167083333, 18.0314159}, constellationPoint{87.326982083333, 18.0435486}, constellationPoint{87.39379375, 22.8764725}, constellationPoint{90.1440375, 22.8430862}, constellationPoint{90.125155416667, 21.5098724}, constellationPoint{95.1241275, 21.4491768}, constellationPoint{95.069575416667, 17.4495068}, constellationPoint{96.4439175, 17.4328651}, constellationPoint{96.37276375, 11.9332972}, constellationPoint{96.347665416667, 9.9334478}, constellationPoint{95.34803125, 9.9455481}, constellationPoint{95.225680416667, -0.0537102}, constellationPoint{95.177142083333, -4.0534163}, constellationPoint{89.052372083333, -3.9790573}, constellationPoint{88.965790416667, -10.9785318}, constellationPoint{77.72003125, -10.8432293}, constellationPoint{77.804395416667, -3.8437285}, constellationPoint{71.55635875, -3.7708201}, constellationPoint{71.60231375, 0.2289162}}
constellationPolygons["PAV"] = []constellationPoint{constellationPoint{274.19506041667, -74.9745178}, constellationPoint{323.1847575, -74.4544678}, constellationPoint{322.34865125, -59.4576836}, constellationPoint{307.56480125, -59.5880547}, constellationPoint{307.45880125, -56.5885773}, constellationPoint{272.67225291667, -56.9837723}, constellationPoint{265.16818958333, -57.0747757}, constellationPoint{265.77572875, -67.5711060}, constellationPoint{273.28007708333, -67.4800797}}
constellationPolygons["PEG"] = []constellationPoint{constellationPoint{321.58347125, 2.5393796}, constellationPoint{321.50110208333, 13.0390635}, constellationPoint{318.244515, 13.0132008}, constellationPoint{318.25026458333, 12.3465548}, constellationPoint{317.24836375, 12.3382607}, constellationPoint{317.17878291667, 20.0046406}, constellationPoint{320.18840875, 20.0290813}, constellationPoint{320.15170041667, 24.0289364}, constellationPoint{322.66201958333, 24.0482101}, constellationPoint{322.62016375, 28.5480537}, constellationPoint{327.39518125, 28.5817947}, constellationPoint{327.319965, 36.5815468}, constellationPoint{329.4610125, 36.5953827}, constellationPoint{331.35046041667, 36.6069069}, constellationPoint{331.35955791667, 35.6069336}, constellationPoint{343.70653208333, 35.6656113}, constellationPoint{343.70919291667, 35.1656151}, constellationPoint{344.46530375, 35.1682358}, constellationPoint{354.04417958333, 35.1913109}, constellationPoint{354.04915791667, 32.7746468}, constellationPoint{357.8280825, 32.7785072}, constellationPoint{357.82874125, 32.0285034}, constellationPoint{1.6069770833333, 32.0293655}, constellationPoint{1.60621625, 28.6960354}, constellationPoint{2.6128425, 28.6957588}, constellationPoint{2.61001625, 22.6957588}, constellationPoint{3.7406445833333, 22.6951923}, constellationPoint{3.73992125, 21.6951923}, constellationPoint{3.7341179166667, 13.1951942}, constellationPoint{1.6031745833333, 13.1960354}, constellationPoint{1.6027304166667, 10.6960354}, constellationPoint{359.09711875, 10.6957970}, constellationPoint{359.09803375, 8.1957970}, constellationPoint{342.82141625, 8.1621685}, constellationPoint{342.84221708333, 2.6622071}, constellationPoint{331.58726708333, 2.6076074}, constellationPoint{331.588755, 2.3576119}, constellationPoint{326.58708625, 2.3256910}, constellationPoint{326.58021875, 3.3256676}, constellationPoint{323.57874875, 3.3043909}, constellationPoint{323.58427041667, 2.5544112}}
constellationPolygons["PER"] = []constellationPoint{constellationPoint{42.62838, 31.1865025}, constellationPoint{42.666467916667, 34.5196762}, constellationPoint{40.402382916667, 34.5375137}, constellationPoint{40.43465875, 37.2873878}, constellationPoint{39.67934125, 37.2931557}, constellationPoint{39.88547875, 51.0423737}, constellationPoint{32.67380125, 51.0925827}, constellationPoint{32.62149125, 47.5927505}, constellationPoint{26.931439583333, 47.6258430}, constellationPoint{26.96852375, 50.6257439}, constellationPoint{22.40793625, 50.6478767}, constellationPoint{22.45601375, 54.6477699}, constellationPoint{27.53364125, 54.6228828}, constellationPoint{27.5952225, 58.1227188}, constellationPoint{30.77362375, 58.1046753}, constellationPoint{30.795625416667, 59.1046104}, constellationPoint{38.802355416667, 59.0511551}, constellationPoint{38.762337083333, 57.5513000}, constellationPoint{48.90093, 57.4684982}, constellationPoint{49.91349625, 57.4593849}, constellationPoint{49.8540225, 55.4596519}, constellationPoint{52.381900416667, 55.4362831}, constellationPoint{52.31308625, 52.9366074}, constellationPoint{72.840285, 52.7196465}, constellationPoint{72.45734375, 36.2218513}, constellationPoint{69.57384125, 36.2547150}, constellationPoint{69.4869375, 30.9218750}, constellationPoint{52.426667916667, 31.1003609}}
constellationPolygons["PHE"] = []constellationPoint{constellationPoint{351.69270458333, -39.3127594}, constellationPoint{351.76852208333, -56.3126869}, constellationPoint{351.77839375, -57.8126793}, constellationPoint{21.20622875, -57.8484154}, constellationPoint{21.2732625, -52.8485603}, constellationPoint{24.967354583333, -52.8658562}, constellationPoint{24.9938475, -50.8659210}, constellationPoint{28.693257083333, -50.8859215}, constellationPoint{28.738322916667, -47.5527229}, constellationPoint{36.1529475, -47.6004944}, constellationPoint{36.26401375, -39.4342155}, constellationPoint{26.350727916667, -39.3726234}}
constellationPolygons["PIC"] = []constellationPoint{constellationPoint{90.951777083333, -43.0057793}, constellationPoint{75.97444375, -42.8255501}, constellationPoint{73.482370416667, -42.7963676}, constellationPoint{73.402082916667, -46.2959023}, constellationPoint{68.42409625, -46.2387962}, constellationPoint{68.362247916667, -48.7384491}, constellationPoint{68.217745416667, -53.7376366}, constellationPoint{75.6770175, -53.8238029}, constellationPoint{75.547742916667, -57.3230400}, constellationPoint{83.01880375, -57.4122620}, constellationPoint{82.85761375, -60.9112892}, constellationPoint{90.34506125, -61.0020981}, constellationPoint{90.173642916667, -64.0010529}, constellationPoint{98.93724875, -64.1070251}, constellationPoint{102.70331375, -64.1518784}, constellationPoint{103.01111708333, -58.1537018}, constellationPoint{97.995077916667, -58.0938416}, constellationPoint{98.114275416667, -55.0945587}, constellationPoint{93.1074, -55.0340500}, constellationPoint{93.19435375, -52.5345764}, constellationPoint{90.693705, -52.5042114}, constellationPoint{90.748902083333, -50.7545471}}
constellationPolygons["PSC"] = []constellationPoint{constellationPoint{342.8497125, 0.6622211}, constellationPoint{342.84221708333, 2.6622071}, constellationPoint{342.82141625, 8.1621685}, constellationPoint{359.09803375, 8.1957970}, constellationPoint{359.09711875, 10.6957970}, constellationPoint{1.6027304166667, 10.6960354}, constellationPoint{1.6031745833333, 13.1960354}, constellationPoint{3.7341179166667, 13.1951942}, constellationPoint{3.73992125, 21.6951923}, constellationPoint{14.414815416667, 21.6766376}, constellationPoint{14.424064583333, 24.4266243}, constellationPoint{12.41349125, 24.4319324}, constellationPoint{12.44306375, 33.6818962}, constellationPoint{22.89742625, 33.6453705}, constellationPoint{22.86642, 28.6454391}, constellationPoint{26.76471, 28.6262817}, constellationPoint{26.744674583333, 25.6263351}, constellationPoint{26.65573375, 10.5432396}, constellationPoint{31.6652475, 10.5143948}, constellationPoint{31.61526625, 2.5978806}, constellationPoint{6.6037875, 2.6925383}, constellationPoint{6.60132875, 0.6925398}, constellationPoint{6.5927025, -6.3074551}, constellationPoint{359.10329875, -6.3042021}, constellationPoint{359.10221125, -3.3042023}, constellationPoint{342.86470375, -3.3377509}}
constellationPolygons["PSA"] = []constellationPoint{constellationPoint{346.68096625, -24.8250446}, constellationPoint{329.77028875, -24.9040413}, constellationPoint{321.80777541667, -24.9597607}, constellationPoint{321.83163625, -27.4596672}, constellationPoint{321.92805291667, -36.4592972}, constellationPoint{346.72751375, -36.3249741}}
constellationPolygons["PUP"] = []constellationPoint{constellationPoint{111.97339958333, -11.2521448}, constellationPoint{111.67719875, -33.2504692}, constellationPoint{99.903859583333, -33.1128159}, constellationPoint{99.70891625, -43.1116486}, constellationPoint{90.951777083333, -43.0057793}, constellationPoint{90.748902083333, -50.7545471}, constellationPoint{120.8616975, -51.1025848}, constellationPoint{121.03827875, -43.3535042}, constellationPoint{126.57231291667, -43.4095192}, constellationPoint{126.67779875, -37.1600380}, constellationPoint{126.92709458333, -17.4112568}, constellationPoint{126.98977958333, -11.4115648}, constellationPoint{122.73417875, -11.3687992}}
constellationPolygons["PYX"] = []constellationPoint{constellationPoint{126.92709458333, -17.4112568}, constellationPoint{130.18434, -17.4424706}, constellationPoint{130.1635125, -19.4423733}, constellationPoint{137.68497, -19.5088310}, constellationPoint{137.63677625, -24.5086308}, constellationPoint{141.904335, -24.5425186}, constellationPoint{141.77159875, -37.2920151}, constellationPoint{126.67779875, -37.1600380}}
constellationPolygons["RET"] = []constellationPoint{constellationPoint{48.362689583333, -67.0358200}, constellationPoint{68.79401875, -67.2479248}, constellationPoint{69.274534583333, -58.7506638}, constellationPoint{65.55459, -58.7088547}, constellationPoint{65.6504625, -56.2093849}, constellationPoint{60.69291875, -56.1555862}, constellationPoint{60.79789625, -52.8228111}, constellationPoint{58.318787916667, -52.7968445}, constellationPoint{53.365002083333, -52.7470779}, constellationPoint{53.23681625, -57.0797844}, constellationPoint{48.79113125, -57.0377846}}
constellationPolygons["SGE"] = []constellationPoint{constellationPoint{284.37363625, 18.6647091}, constellationPoint{284.33913291667, 21.2478352}, constellationPoint{290.09631125, 21.3148155}, constellationPoint{290.12128791667, 19.3982983}, constellationPoint{298.88568875, 19.4955387}, constellationPoint{298.860255, 21.5787334}, constellationPoint{305.12540875, 21.6436558}, constellationPoint{305.13404875, 20.8936996}, constellationPoint{305.18694875, 16.1439629}, constellationPoint{303.5589975, 16.1275158}, constellationPoint{298.926, 16.0790844}, constellationPoint{298.92116541667, 16.4957294}, constellationPoint{286.4054775, 16.3550682}, constellationPoint{286.37549375, 18.6882229}}
constellationPolygons["SGR"] = []constellationPoint{constellationPoint{284.74405375, -11.8664360}, constellationPoint{284.79372, -15.8328123}, constellationPoint{275.54952625, -15.9435720}, constellationPoint{265.80019041667, -16.0618820}, constellationPoint{266.00183541667, -30.0606632}, constellationPoint{269.50281125, -30.0182076}, constellationPoint{269.62546375, -37.0174599}, constellationPoint{289.59631958333, -36.7785645}, constellationPoint{289.76964, -45.2775650}, constellationPoint{307.169295, -45.0900002}, constellationPoint{306.89795541667, -27.5913391}, constellationPoint{301.91596958333, -27.6419144}, constellationPoint{301.72636958333, -11.6762342}}
constellationPolygons["SCO"] = []constellationPoint{constellationPoint{240.43727875, -8.3523235}, constellationPoint{245.69120375, -8.2958899}, constellationPoint{245.81068041667, -18.5452347}, constellationPoint{247.43823, -18.5272255}, constellationPoint{247.45067541667, -19.5271549}, constellationPoint{245.82298375, -19.5451660}, constellationPoint{245.89144625, -24.8781185}, constellationPoint{253.15567041667, -24.7960968}, constellationPoint{253.23534875, -30.2123089}, constellationPoint{266.00183541667, -30.0606632}, constellationPoint{269.50281125, -30.0182076}, constellationPoint{269.62546375, -37.0174599}, constellationPoint{269.80928375, -45.5163460}, constellationPoint{248.57062375, -45.7670517}, constellationPoint{248.4947775, -42.2674789}, constellationPoint{242.15280625, -42.3366776}, constellationPoint{241.94769875, -29.8377628}, constellationPoint{236.92998, -29.8896160}, constellationPoint{236.81306375, -20.3902016}, constellationPoint{240.57177625, -20.3516178}}
constellationPolygons["SCL"] = []constellationPoint{constellationPoint{346.68096625, -24.8250446}, constellationPoint{359.11056458333, -24.8042011}, constellationPoint{26.45888875, -24.8729095}, constellationPoint{26.350727916667, -39.3726234}, constellationPoint{351.69270458333, -39.3127594}, constellationPoint{351.6833775, -36.3127670}, constellationPoint{346.72751375, -36.3249741}}
constellationPolygons["SCT"] = []constellationPoint{constellationPoint{275.54952625, -15.9435720}, constellationPoint{284.79372, -15.8328123}, constellationPoint{284.74405375, -11.8664360}, constellationPoint{284.64729291667, -3.8336766}, constellationPoint{280.3981875, -3.8842230}, constellationPoint{275.3991225, -3.9444826}}
constellationPolygons["SER1"] = []constellationPoint{constellationPoint{227.85301208333, -0.4742887}, constellationPoint{227.78148625, 7.5253930}, constellationPoint{227.60549125, 25.5246105}, constellationPoint{229.09951625, 25.5380573}, constellationPoint{241.80573458333, 25.6641407}, constellationPoint{241.85657541667, 21.6644115}, constellationPoint{240.1110075, 21.6459675}, constellationPoint{240.18107375, 15.6463346}, constellationPoint{242.67663, 15.6728001}, constellationPoint{242.80966791667, 3.6735139}, constellationPoint{245.558595, 3.7033811}, constellationPoint{245.6026275, -0.2963768}, constellationPoint{245.63838875, -3.5461800}, constellationPoint{240.38695375, -3.6025870}, constellationPoint{227.88195125, -3.7241600}}
constellationPolygons["SER2"] = []constellationPoint{constellationPoint{275.35059875, 0.0552235}, constellationPoint{275.31423625, 3.0550034}, constellationPoint{277.93922375, 3.0867271}, constellationPoint{277.92105625, 4.5866175}, constellationPoint{275.29601125, 4.5548930}, constellationPoint{275.27461041667, 6.3047633}, constellationPoint{281.45856875, 6.3791943}, constellationPoint{284.525985, 6.4156075}, constellationPoint{284.57642541667, 2.1659052}, constellationPoint{280.3262325, 2.1153460}, constellationPoint{280.35020875, 0.1154895}, constellationPoint{280.3981875, -3.8842230}, constellationPoint{275.3991225, -3.9444826}, constellationPoint{275.54952625, -15.9435720}, constellationPoint{265.80019041667, -16.0618820}, constellationPoint{259.29742791667, -16.1399899}, constellationPoint{259.22206958333, -10.1404381}, constellationPoint{265.47349041667, -10.0653696}, constellationPoint{265.49440375, -11.7319136}, constellationPoint{266.7447, -11.7167768}, constellationPoint{266.72375708333, -10.0502338}, constellationPoint{271.22371625, -9.9956055}, constellationPoint{271.14967375, -3.9960551}, constellationPoint{269.14970375, -4.0203514}, constellationPoint{269.10103791667, -0.0206471}}
constellationPolygons["SEX"] = []constellationPoint{constellationPoint{145.34890625, -0.5670585}, constellationPoint{145.39841708333, 6.4327669}, constellationPoint{162.87601958333, 6.3377299}, constellationPoint{162.8497125, -0.6622211}, constellationPoint{162.82713875, -6.6621790}, constellationPoint{162.80791375, -11.6621428}, constellationPoint{145.27027208333, -11.5667810}}
constellationPolygons["TAU"] = []constellationPoint{constellationPoint{50.836682916667, -1.3029516}, constellationPoint{50.85298375, 0.4469725}, constellationPoint{50.94641125, 10.3632069}, constellationPoint{51.037234583333, 19.4461136}, constellationPoint{52.2906225, 19.4343338}, constellationPoint{52.426667916667, 31.1003609}, constellationPoint{69.4869375, 30.9218750}, constellationPoint{69.47678875, 30.2552605}, constellationPoint{73.235342916667, 30.2123089}, constellationPoint{73.212475416667, 28.7124405}, constellationPoint{90.228902916667, 28.5092430}, constellationPoint{90.22107125, 28.0092907}, constellationPoint{90.1440375, 22.8430862}, constellationPoint{87.39379375, 22.8764725}, constellationPoint{87.326982083333, 18.0435486}, constellationPoint{88.327167083333, 18.0314159}, constellationPoint{88.255369583333, 12.5318508}, constellationPoint{85.755057083333, 12.5621548}, constellationPoint{85.79364625, 15.5619202}, constellationPoint{81.7922325, 15.6101446}, constellationPoint{81.7987125, 16.1101055}, constellationPoint{76.29527875, 16.1754990}, constellationPoint{76.28892625, 15.6755352}, constellationPoint{71.03402875, 15.7364635}, constellationPoint{70.852360416667, 0.2375014}, constellationPoint{55.352905416667, 0.4037257}, constellationPoint{55.335582083333, -1.3461887}}
constellationPolygons["TEL"] = []constellationPoint{constellationPoint{307.45880125, -56.5885773}, constellationPoint{307.169295, -45.0900002}, constellationPoint{289.76964, -45.2775650}, constellationPoint{272.3090175, -45.4859734}, constellationPoint{272.67225291667, -56.9837723}}
constellationPolygons["TRI"] = []constellationPoint{constellationPoint{26.744674583333, 25.6263351}, constellationPoint{26.76471, 28.6262817}, constellationPoint{22.86642, 28.6454391}, constellationPoint{22.89742625, 33.6453705}, constellationPoint{22.910835, 35.6453362}, constellationPoint{31.854250416667, 35.5971375}, constellationPoint{31.87109125, 37.3470840}, constellationPoint{39.67934125, 37.2931557}, constellationPoint{40.43465875, 37.2873878}, constellationPoint{40.402382916667, 34.5375137}, constellationPoint{42.666467916667, 34.5196762}, constellationPoint{42.62838, 31.1865025}, constellationPoint{38.10319375, 31.2213154}, constellationPoint{38.07014875, 27.8047638}, constellationPoint{30.53061625, 27.8550186}, constellationPoint{30.51371125, 25.6050701}}
constellationPolygons["TRA"] = []constellationPoint{constellationPoint{224.16644125, -70.5115433}, constellationPoint{224.00363375, -68.0122070}, constellationPoint{226.55712625, -67.9909286}, constellationPoint{226.35353541667, -64.0751266}, constellationPoint{230.16657875, -64.0415649}, constellationPoint{230.05456958333, -61.4587479}, constellationPoint{232.58976458333, -61.4353065}, constellationPoint{232.5498675, -60.4354935}, constellationPoint{249.03468125, -60.2644577}, constellationPoint{249.08163, -61.2641945}, constellationPoint{251.53784708333, -61.2364578}, constellationPoint{251.67632125, -63.8189964}, constellationPoint{254.1951375, -63.7900925}, constellationPoint{254.28351458333, -65.2062531}, constellationPoint{255.5423925, -65.1916428}, constellationPoint{255.72498291667, -67.6905823}, constellationPoint{258.2424825, -67.6610870}, constellationPoint{258.47067875, -70.1597443}}
constellationPolygons["TUC"] = []constellationPoint{constellationPoint{351.99783291667, -74.3124619}, constellationPoint{1.5662970833333, -74.3039627}, constellationPoint{12.3324375, -74.3185730}, constellationPoint{12.295414583333, -75.3185272}, constellationPoint{20.654050416667, -75.3472214}, constellationPoint{21.20622875, -57.8484154}, constellationPoint{351.77839375, -57.8126793}, constellationPoint{351.76852208333, -56.3126869}, constellationPoint{332.113695, -56.3908348}, constellationPoint{332.3985675, -66.8899918}, constellationPoint{351.86139125, -66.8125992}}
constellationPolygons["UMA"] = []constellationPoint{constellationPoint{145.70923791667, 41.4316750}, constellationPoint{139.51249041667, 41.4785957}, constellationPoint{139.59071125, 46.4782791}, constellationPoint{128.44010375, 46.5777283}, constellationPoint{128.79913375, 59.5759888}, constellationPoint{122.12910125, 59.6433983}, constellationPoint{123.08622875, 73.1383743}, constellationPoint{140.61547375, 72.9741364}, constellationPoint{171.96136958333, 72.8125000}, constellationPoint{171.84934625, 65.8126068}, constellationPoint{181.57925541667, 65.8039627}, constellationPoint{181.58155958333, 63.3039627}, constellationPoint{203.55053875, 63.3593445}, constellationPoint{203.57364125, 62.3593979}, constellationPoint{217.04525375, 62.4414825}, constellationPoint{217.25124875, 54.9422379}, constellationPoint{211.58439125, 54.9035759}, constellationPoint{211.69873208333, 47.9039383}, constellationPoint{203.79511375, 47.8599281}, constellationPoint{203.74239875, 52.3598061}, constellationPoint{182.8185225, 52.3043365}, constellationPoint{182.82643375, 44.3043365}, constellationPoint{181.59141625, 44.3039627}, constellationPoint{181.59450625, 33.3039627}, constellationPoint{181.59566375, 28.3039627}, constellationPoint{179.60894125, 28.3040466}, constellationPoint{166.69399791667, 28.3250256}, constellationPoint{166.71422541667, 33.3249931}, constellationPoint{163.48940875, 33.3356781}, constellationPoint{163.52316875, 39.3356133}, constellationPoint{154.3594125, 39.3774109}, constellationPoint{154.37822375, 41.3773613}}
constellationPolygons["UMI"] = []constellationPoint{constellationPoint{195.8206125, 76.3289108}, constellationPoint{196.09747375, 69.3293610}, constellationPoint{210.65081125, 69.3991165}, constellationPoint{210.82055541667, 65.3996506}, constellationPoint{235.32956541667, 65.6023483}, constellationPoint{235.05063, 69.6009445}, constellationPoint{247.8410625, 69.7383041}, constellationPoint{247.2207075, 74.7347870}, constellationPoint{261.53663708333, 74.9033127}, constellationPoint{260.21790458333, 79.8953476}, constellationPoint{267.65602041667, 79.9857483}, constellationPoint{261.72223041667, 85.9495697}, constellationPoint{308.72097, 86.4656219}, constellationPoint{308.33135541667, 86.6306305}, constellationPoint{343.51066625, 86.8368912}, constellationPoint{339.26098791667, 88.6638870}, constellationPoint{135.83247125, 87.5689163}, constellationPoint{130.40275041667, 86.0975418}, constellationPoint{213.0229575, 85.9308090}, constellationPoint{216.78285625, 79.4449844}, constellationPoint{203.80918958333, 79.3629303}, constellationPoint{204.15701875, 76.3638153}}
constellationPolygons["VEL"] = []constellationPoint{constellationPoint{166.33725625, -57.1744423}, constellationPoint{166.45650458333, -40.4246216}, constellationPoint{141.73406125, -40.2918739}, constellationPoint{141.77159875, -37.2920151}, constellationPoint{126.67779875, -37.1600380}, constellationPoint{126.57231291667, -43.4095192}, constellationPoint{121.03827875, -43.3535042}, constellationPoint{120.8616975, -51.1025848}, constellationPoint{123.38112875, -51.1285286}, constellationPoint{123.32011625, -53.3782196}, constellationPoint{127.60929125, -53.4206772}, constellationPoint{127.56711875, -54.9204712}, constellationPoint{133.38017375, -54.9742203}, constellationPoint{133.32365541667, -56.9739723}}
constellationPolygons["VIR"] = []constellationPoint{constellationPoint{174.35052458333, -0.6916979}, constellationPoint{174.36568791667, 10.3082914}, constellationPoint{179.60373458333, 10.3040485}, constellationPoint{179.60453541667, 13.3040485}, constellationPoint{194.0620275, 13.3225126}, constellationPoint{194.05906625, 14.3225088}, constellationPoint{204.02893041667, 14.3604937}, constellationPoint{204.06384875, 7.3605771}, constellationPoint{227.78148625, 7.5253930}, constellationPoint{227.85301208333, -0.4742887}, constellationPoint{221.6030925, -0.5269387}, constellationPoint{221.66710791667, -8.5266848}, constellationPoint{215.40850625, -8.5731344}, constellationPoint{215.51309125, -22.5727749}, constellationPoint{194.16687, -22.6773415}, constellationPoint{194.1330525, -11.6773882}, constellationPoint{179.09676, -11.6957970}, constellationPoint{179.09860625, -6.6957974}, constellationPoint{174.34229875, -6.6916924}}
constellationPolygons["VOL"] = []constellationPoint{constellationPoint{98.93724875, -64.1070251}, constellationPoint{98.454422916667, -70.1041336}, constellationPoint{97.770709583333, -75.1000366}, constellationPoint{114.21470375, -75.2899170}, constellationPoint{135.24368708333, -75.4954681}, constellationPoint{136.09472708333, -64.4990387}, constellationPoint{102.70331375, -64.1518784}}
constellationPolygons["VUL"] = []constellationPoint{constellationPoint{284.33913291667, 21.2478352}, constellationPoint{284.27716208333, 25.6641407}, constellationPoint{290.16131375, 25.7325745}, constellationPoint{290.13264625, 27.7324085}, constellationPoint{296.27220125, 27.8011742}, constellationPoint{296.25094375, 29.3010578}, constellationPoint{315.07258375, 29.4871387}, constellationPoint{315.08391458333, 28.4871883}, constellationPoint{322.62016375, 28.5480537}, constellationPoint{322.66201958333, 24.0482101}, constellationPoint{320.15170041667, 24.0289364}, constellationPoint{320.18840875, 20.0290813}, constellationPoint{317.17878291667, 20.0046406}, constellationPoint{309.907665, 19.9399967}, constellationPoint{309.89693708333, 20.9399471}, constellationPoint{305.13404875, 20.8936996}, constellationPoint{305.12540875, 21.6436558}, constellationPoint{298.860255, 21.5787334}, constellationPoint{298.88568875, 19.4955387}, constellationPoint{290.12128791667, 19.3982983}, constellationPoint{290.09631125, 21.3148155}}
change := []string{"PSC", "TUC", "PHE", "SCL", "CET", "PEG", "AND", "CAS", "CEP"}
for _, v := range change {
for k, v2 := range constellationPolygons[v] {
if v2.RA < 270 {
constellationPolygons[v][k].RA = v2.RA + 360
}
}
}
}
//选定 RA=277.5 DEC=-40
func isCross(a, b, c, d constellationPoint) bool {
var ac, bc, ad, bd, ca, cb, da, db constellationPoint
var r1, r2 float64
ac.RA = a.RA - c.RA
ac.DEC = a.DEC - c.DEC
ad.RA = a.RA - d.RA
ad.DEC = a.DEC - d.DEC
r1 = ac.RA*ad.DEC - ad.RA*ac.DEC
bc.RA = b.RA - c.RA
bc.DEC = b.DEC - c.DEC
bd.RA = b.RA - d.RA
bd.DEC = b.DEC - d.DEC
r2 = bc.RA*bd.DEC - bd.RA*bc.DEC
//echo r1.' '.r2;
if r1*r2 > 0 {
return false
}
ca.RA = c.RA - a.RA
ca.DEC = c.DEC - a.DEC
cb.RA = c.RA - b.RA
cb.DEC = c.DEC - b.DEC
r1 = ca.RA*cb.DEC - cb.RA*ca.DEC
da.RA = d.RA - a.RA
da.DEC = d.DEC - a.DEC
db.RA = d.RA - b.RA
db.DEC = d.DEC - b.DEC
r2 = da.RA*db.DEC - db.RA*da.DEC
if r1*r2 > 0 {
return false
}
return true
}
func resolveConstellationCode(ra, dec, jde float64) string {
var nra, ndec float64
initConstellationData()
nra = ra
if ra >= 360 {
nra -= 360
}
nra, ndec = Precess(nra, dec, jde, 2451545.0)
if ra >= 360 && nra < 270 {
nra += 360
}
if code := matchConstellationCode(nra, ndec); code != "" {
return code
}
if nra <= 270 {
ra = ra + 360
return resolveConstellationCode(ra, dec, jde)
}
if ndec > 50 {
return "UMI"
} else if ndec < -50 {
return "OCT"
}
return ""
}
func matchConstellationCode(ra, dec float64) string {
target := constellationPoint{RA: ra, DEC: dec}
for _, boundary := range constellationBoundaries {
if ra < boundary.minRA || ra > boundary.maxRA || dec < boundary.minDec || dec > boundary.maxDec {
continue
}
count := 0
pointCount := len(boundary.points)
for index := 0; index < pointCount-1; index++ {
if index == 0 && isCross(constellationRayStart, target, boundary.points[pointCount-1], boundary.points[0]) {
count++
}
if isCross(constellationRayStart, target, boundary.points[index], boundary.points[index+1]) {
count++
}
if FR((ra-constellationRayStart.RA)*(boundary.points[index].DEC-constellationRayStart.DEC)) == FR((boundary.points[index].RA-constellationRayStart.RA)*(dec-constellationRayStart.DEC)) {
count++
}
}
if count%2 == 1 {
return boundary.code
}
}
return ""
}
func ConstellationNameZH(ra, dec, jde float64) string {
return ConstellationNameByCodeZH(ConstellationCode(ra, dec, jde))
}
+182
View File
@@ -0,0 +1,182 @@
package basic
import "sync"
type constellationMeta struct {
code string
zh string
en string
}
type constellationBoundary struct {
code string
points []constellationPoint
minRA float64
maxRA float64
minDec float64
maxDec float64
}
var (
constellationInitOnce sync.Once
constellationMetas = [...]constellationMeta{
{code: "AND", zh: "仙女座", en: "Andromeda"},
{code: "ANT", zh: "唧筒座", en: "Antlia"},
{code: "APS", zh: "天燕座", en: "Apus"},
{code: "AQR", zh: "宝瓶座", en: "Aquarius"},
{code: "AQL", zh: "天鹰座", en: "Aquila"},
{code: "ARA", zh: "天坛座", en: "Ara"},
{code: "ARI", zh: "白羊座", en: "Aries"},
{code: "AUR", zh: "御夫座", en: "Auriga"},
{code: "BOO", zh: "牧夫座", en: "Bootes"},
{code: "CAE", zh: "雕具座", en: "Caelum"},
{code: "CAM", zh: "鹿豹座", en: "Camelopardalis"},
{code: "CNC", zh: "巨蟹座", en: "Cancer"},
{code: "CVN", zh: "猎犬座", en: "Canes Venatici"},
{code: "CMA", zh: "大犬座", en: "Canis Major"},
{code: "CMI", zh: "小犬座", en: "Canis Minor"},
{code: "CAP", zh: "摩羯座", en: "Capricornus"},
{code: "CAR", zh: "船底座", en: "Carina"},
{code: "CAS", zh: "仙后座", en: "Cassiopeia"},
{code: "CEN", zh: "半人马座", en: "Centaurus"},
{code: "CEP", zh: "仙王座", en: "Cepheus"},
{code: "CET", zh: "鲸鱼座", en: "Cetus"},
{code: "CHA", zh: "蝘蜓座", en: "Chamaeleon"},
{code: "CIR", zh: "圆规座", en: "Circinus"},
{code: "COL", zh: "天鸽座", en: "Columba"},
{code: "COM", zh: "后发座", en: "Coma Berenices"},
{code: "CRA", zh: "南冕座", en: "Corona Australis"},
{code: "CRB", zh: "北冕座", en: "Corona Borealis"},
{code: "CRV", zh: "乌鸦座", en: "Corvus"},
{code: "CRT", zh: "巨爵座", en: "Crater"},
{code: "CRU", zh: "南十字座", en: "Crux"},
{code: "CYG", zh: "天鹅座", en: "Cygnus"},
{code: "DEL", zh: "海豚座", en: "Delphinus"},
{code: "DOR", zh: "剑鱼座", en: "Dorado"},
{code: "DRA", zh: "天龙座", en: "Draco"},
{code: "EQU", zh: "小马座", en: "Equuleus"},
{code: "ERI", zh: "波江座", en: "Eridanus"},
{code: "FOR", zh: "天炉座", en: "Fornax"},
{code: "GEM", zh: "双子座", en: "Gemini"},
{code: "GRU", zh: "天鹤座", en: "Grus"},
{code: "HER", zh: "武仙座", en: "Hercules"},
{code: "HOR", zh: "时钟座", en: "Horologium"},
{code: "HYA", zh: "长蛇座", en: "Hydra"},
{code: "HYI", zh: "水蛇座", en: "Hydrus"},
{code: "IND", zh: "印第安座", en: "Indus"},
{code: "LAC", zh: "蝎虎座", en: "Lacerta"},
{code: "LEO", zh: "狮子座", en: "Leo"},
{code: "LMI", zh: "小狮座", en: "Leo Minor"},
{code: "LEP", zh: "天兔座", en: "Lepus"},
{code: "LIB", zh: "天秤座", en: "Libra"},
{code: "LUP", zh: "豺狼座", en: "Lupus"},
{code: "LYN", zh: "天猫座", en: "Lynx"},
{code: "LYR", zh: "天琴座", en: "Lyra"},
{code: "MEN", zh: "山案座", en: "Mensa"},
{code: "MIC", zh: "显微镜座", en: "Microscopium"},
{code: "MON", zh: "麒麟座", en: "Monoceros"},
{code: "MUS", zh: "苍蝇座", en: "Musca"},
{code: "NOR", zh: "矩尺座", en: "Norma"},
{code: "OCT", zh: "南极座", en: "Octans"},
{code: "OPH", zh: "蛇夫座", en: "Ophiuchus"},
{code: "ORI", zh: "猎户座", en: "Orion"},
{code: "PAV", zh: "孔雀座", en: "Pavo"},
{code: "PEG", zh: "飞马座", en: "Pegasus"},
{code: "PER", zh: "英仙座", en: "Perseus"},
{code: "PHE", zh: "凤凰座", en: "Phoenix"},
{code: "PIC", zh: "绘架座", en: "Pictor"},
{code: "PSC", zh: "双鱼座", en: "Pisces"},
{code: "PSA", zh: "南鱼座", en: "Piscis Austrinus"},
{code: "PUP", zh: "船尾座", en: "Puppis"},
{code: "PYX", zh: "罗盘座", en: "Pyxis"},
{code: "RET", zh: "网罟座", en: "Reticulum"},
{code: "SGE", zh: "天箭座", en: "Sagitta"},
{code: "SGR", zh: "人马座", en: "Sagittarius"},
{code: "SCO", zh: "天蝎座", en: "Scorpius"},
{code: "SCL", zh: "玉夫座", en: "Sculptor"},
{code: "SCT", zh: "盾牌座", en: "Scutum"},
{code: "SER1", zh: "巨蛇座", en: "Serpens Caput"},
{code: "SER2", zh: "巨蛇座", en: "Serpens Cauda"},
{code: "SEX", zh: "六分仪座", en: "Sextans"},
{code: "TAU", zh: "金牛座", en: "Taurus"},
{code: "TEL", zh: "望远镜座", en: "Telescopium"},
{code: "TRI", zh: "三角座", en: "Triangulum"},
{code: "TRA", zh: "南三角座", en: "Triangulum Australe"},
{code: "TUC", zh: "杜鹃座", en: "Tucana"},
{code: "UMA", zh: "大熊座", en: "Ursa Major"},
{code: "UMI", zh: "小熊座", en: "Ursa Minor"},
{code: "VEL", zh: "船帆座", en: "Vela"},
{code: "VIR", zh: "室女座", en: "Virgo"},
{code: "VOL", zh: "飞鱼座", en: "Volans"},
{code: "VUL", zh: "狐狸座", en: "Vulpecula"},
}
constellationNameZH map[string]string
constellationNameEN map[string]string
constellationBoundaries []constellationBoundary
constellationRayStart = constellationPoint{RA: 277.5, DEC: -100}
)
func initConstellationData() {
constellationInitOnce.Do(func() {
initConstellationPolygons()
constellationNameZH = make(map[string]string, len(constellationMetas))
constellationNameEN = make(map[string]string, len(constellationMetas))
constellationBoundaries = make([]constellationBoundary, 0, len(constellationMetas)-2)
for _, meta := range constellationMetas {
constellationNameZH[meta.code] = meta.zh
constellationNameEN[meta.code] = meta.en
if meta.code == "UMI" || meta.code == "OCT" {
continue
}
points := constellationPolygons[meta.code]
if len(points) == 0 {
continue
}
boundary := constellationBoundary{
code: meta.code,
points: points,
minRA: points[0].RA,
maxRA: points[0].RA,
minDec: points[0].DEC,
maxDec: points[0].DEC,
}
for _, point := range points[1:] {
if point.RA < boundary.minRA {
boundary.minRA = point.RA
}
if point.RA > boundary.maxRA {
boundary.maxRA = point.RA
}
if point.DEC < boundary.minDec {
boundary.minDec = point.DEC
}
if point.DEC > boundary.maxDec {
boundary.maxDec = point.DEC
}
}
constellationBoundaries = append(constellationBoundaries, boundary)
}
})
}
// ConstellationCode returns the IAU constellation code / 返回 IAU 星座代码。
func ConstellationCode(ra, dec, jde float64) string {
return resolveConstellationCode(ra, dec, jde)
}
// ConstellationNameEN returns the English constellation name / 返回英文星座名。
func ConstellationNameEN(ra, dec, jde float64) string {
return ConstellationNameByCodeEN(ConstellationCode(ra, dec, jde))
}
// ConstellationNameByCodeZH returns the Chinese name for a code / 返回星座代码对应的中文名。
func ConstellationNameByCodeZH(code string) string {
initConstellationData()
return constellationNameZH[code]
}
// ConstellationNameByCodeEN returns the English name for a code / 返回星座代码对应的英文名。
func ConstellationNameByCodeEN(code string) string {
initConstellationData()
return constellationNameEN[code]
}
+72
View File
@@ -0,0 +1,72 @@
package basic
import (
_ "embed"
"encoding/json"
"testing"
"time"
)
type constellationBaselineSample struct {
RA float64 `json:"ra"`
Dec float64 `json:"dec"`
JDE float64 `json:"jde"`
Code string `json:"code"`
ZH string `json:"zh"`
}
//go:embed testdata/cst_baseline.json
var constellationBaselineJSON []byte
func loadCstBaseline(t *testing.T) []constellationBaselineSample {
t.Helper()
var samples []constellationBaselineSample
if err := json.Unmarshal(constellationBaselineJSON, &samples); err != nil {
t.Fatalf("unmarshal constellation baseline: %v", err)
}
return samples
}
func TestConstellationBaseline(t *testing.T) {
samples := loadCstBaseline(t)
for index, sample := range samples {
if code := resolveConstellationCode(sample.RA, sample.Dec, sample.JDE); code != sample.Code {
t.Fatalf("sample %d code mismatch: ra=%.6f dec=%.6f jde=%.6f got=%q want=%q", index, sample.RA, sample.Dec, sample.JDE, code, sample.Code)
}
if code := ConstellationCode(sample.RA, sample.Dec, sample.JDE); code != sample.Code {
t.Fatalf("sample %d wrapper mismatch: ra=%.6f dec=%.6f jde=%.6f got=%q want=%q", index, sample.RA, sample.Dec, sample.JDE, code, sample.Code)
}
if zh := ConstellationNameZH(sample.RA, sample.Dec, sample.JDE); zh != sample.ZH {
t.Fatalf("sample %d zh mismatch: ra=%.6f dec=%.6f jde=%.6f got=%q want=%q", index, sample.RA, sample.Dec, sample.JDE, zh, sample.ZH)
}
}
}
func TestConstellationNameLookups(t *testing.T) {
tests := []struct {
code string
zh string
en string
}{
{code: "ORI", zh: "猎户座", en: "Orion"},
{code: "PSC", zh: "双鱼座", en: "Pisces"},
{code: "SER1", zh: "巨蛇座", en: "Serpens Caput"},
{code: "SER2", zh: "巨蛇座", en: "Serpens Cauda"},
{code: "UMI", zh: "小熊座", en: "Ursa Minor"},
}
for _, testCase := range tests {
if zh := ConstellationNameByCodeZH(testCase.code); zh != testCase.zh {
t.Fatalf("ConstellationNameByCodeZH(%q) = %q, want %q", testCase.code, zh, testCase.zh)
}
if en := ConstellationNameByCodeEN(testCase.code); en != testCase.en {
t.Fatalf("ConstellationNameByCodeEN(%q) = %q, want %q", testCase.code, en, testCase.en)
}
}
}
func TestConstellationNameEN(t *testing.T) {
jde := Date2JDE(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))
if en := ConstellationNameEN(88.792939, 7.407064, jde); en != "Orion" {
t.Fatalf("ConstellationNameEN() = %q, want %q", en, "Orion")
}
}
@@ -4,21 +4,21 @@ import (
"testing"
)
func Test_Isxz(t *testing.T) {
func TestConstellationNameZH(t *testing.T) {
now := GetNowJDE()
//finish on 30s
for i := 0.00; i <= 360.00; i += 0.5 {
for j := -90.00; j <= 90.00; j += 0.5 {
WhichCst(float64(i), float64(j), now)
ConstellationNameZH(float64(i), float64(j), now)
}
}
}
func Benchmark_IsXZ(b *testing.B) {
func BenchmarkConstellationNameZH(b *testing.B) {
jde := GetNowJDE()
for i := 0; i < b.N; i++ {
//GetNowJDE()
WhichCst(11.11, 12.12, jde)
ConstellationNameZH(11.11, 12.12, jde)
}
}
+25 -25
View File
@@ -10,7 +10,7 @@ import (
* 坐标变换,黄道转赤道
*/
func LoToRa(jde, lo, bo float64) float64 {
ra := math.Atan2(Sin(lo)*Cos(Sita(jde))-Tan(bo)*Sin(Sita(jde)), Cos(lo))
ra := math.Atan2(Sin(lo)*Cos(TrueObliquity(jde))-Tan(bo)*Sin(TrueObliquity(jde)), Cos(lo))
ra = ra * 180 / math.Pi
if ra < 0 {
ra += 360
@@ -19,13 +19,13 @@ func LoToRa(jde, lo, bo float64) float64 {
}
func BoToDec(jde, lo, bo float64) float64 {
dec := ArcSin(Sin(bo)*Cos(Sita(jde)) + Cos(bo)*Sin(Sita(jde))*Sin(lo))
dec := ArcSin(Sin(bo)*Cos(TrueObliquity(jde)) + Cos(bo)*Sin(TrueObliquity(jde))*Sin(lo))
return dec
}
func LoBoToRaDec(jde, lo, bo float64) (float64, float64) {
dec := ArcSin(Sin(bo)*Cos(Sita(jde)) + Cos(bo)*Sin(Sita(jde))*Sin(lo))
ra := math.Atan2(Sin(lo)*Cos(Sita(jde))-Tan(bo)*Sin(Sita(jde)), Cos(lo))
dec := ArcSin(Sin(bo)*Cos(TrueObliquity(jde)) + Cos(bo)*Sin(TrueObliquity(jde))*Sin(lo))
ra := math.Atan2(Sin(lo)*Cos(TrueObliquity(jde))-Tan(bo)*Sin(TrueObliquity(jde)), Cos(lo))
ra = ra * 180 / math.Pi
if ra < 0 {
ra += 360
@@ -36,9 +36,9 @@ func LoBoToRaDec(jde, lo, bo float64) (float64, float64) {
func RaDecToLoBo(jde, ra, dec float64) (float64, float64) {
//tan(λ) = (sin(α)*cos(ε) + tan(δ)*sin(ε)) / cos(α)
//sin(β)=sin(δ)*cos(ε)-cos(δ)*sin(ε)*sin(α)
sita := Sita(jde)
sinBo := Sin(dec)*Cos(sita) - Cos(dec)*Sin(sita)*Sin(ra)
lo := math.Atan2((Sin(ra)*Cos(sita) + Tan(dec)*Sin(sita)), Cos(ra))
eps := TrueObliquity(jde)
sinBo := Sin(dec)*Cos(eps) - Cos(dec)*Sin(eps)*Sin(ra)
lo := math.Atan2((Sin(ra)*Cos(eps) + Tan(dec)*Sin(eps)), Cos(ra))
lo = Limit360(lo * 180 / math.Pi)
return lo, ArcSin(sinBo)
}
@@ -46,8 +46,8 @@ func RaDecToLoBo(jde, ra, dec float64) (float64, float64) {
func RaToLo(jde, ra, dec float64) float64 {
//tan(λ) = (sin(α)*cos(ε) + tan(δ)*sin(ε)) / cos(α)
//sin(β)=sin(δ)*cos(ε)-cos(δ)*sin(ε)*sin(α)
sita := Sita(jde)
lo := math.Atan2((Sin(ra)*Cos(sita) + Tan(dec)*Sin(sita)), Cos(ra))
eps := TrueObliquity(jde)
lo := math.Atan2((Sin(ra)*Cos(eps) + Tan(dec)*Sin(eps)), Cos(ra))
lo = Limit360(lo * 180 / math.Pi)
return lo
}
@@ -55,8 +55,8 @@ func RaToLo(jde, ra, dec float64) float64 {
func DecToBo(jde, ra, dec float64) float64 {
//tan(λ) = (sin(α)*cos(ε) + tan(δ)*sin(ε)) / cos(α)
//sin(β)=sin(δ)*cos(ε)-cos(δ)*sin(ε)*sin(α)
sita := Sita(jde)
sinBo := Sin(dec)*Cos(sita) - Cos(dec)*Sin(sita)*Sin(ra)
eps := TrueObliquity(jde)
sinBo := Sin(dec)*Cos(eps) - Cos(dec)*Sin(eps)*Sin(ra)
return ArcSin(sinBo)
}
@@ -80,7 +80,7 @@ func psini(lat, h float64) float64 {
return psin
}
func ZhanXinRaDec(ra, dec, lat, lon, jd, au, h float64) (float64, float64) {
func TopocentricRaDec(ra, dec, lat, lon, jd, au, h float64) (float64, float64) {
sinpi := Sin(0.0024427777777) / au
pcosi := pcosi(lat, h)
psini := psini(lat, h)
@@ -91,14 +91,14 @@ func ZhanXinRaDec(ra, dec, lat, lon, jd, au, h float64) (float64, float64) {
return ra + nra, ndec
}
func ZhanXinRa(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
func TopocentricRa(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
sinpi := Sin(0.0024427777777) / au
pcosi := pcosi(lat, h)
tH := Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + lon - ra)
nra := math.Atan2(-pcosi*sinpi*Sin(tH), (Cos(dec)-pcosi*sinpi*Cos(tH))) * 180 / math.Pi
return ra + nra
}
func ZhanXinDec(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
func TopocentricDec(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
sinpi := Sin(0.0024427777777) / au
pcosi := pcosi(lat, h)
@@ -110,26 +110,26 @@ func ZhanXinDec(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格林尼
return ndec
}
func ZhanXinLo(lo, bo, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
C := pcosi(lat, h)
S := psini(lat, h)
func TopocentricLo(lo, bo, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
c := pcosi(lat, h)
s := psini(lat, h)
sinpi := Sin(0.0024427777777) / au
ra := LoToRa(jd, lo, bo)
tH := Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + lon - ra)
N := Cos(lo)*Cos(bo) - C*sinpi*Cos(tH)
nlo := math.Atan2(Sin(lo)*Cos(bo)-sinpi*(S*Sin(Sita(jd))+C*Cos(Sita(jd))*Sin(tH)), N) * 180 / math.Pi
n := Cos(lo)*Cos(bo) - c*sinpi*Cos(tH)
nlo := math.Atan2(Sin(lo)*Cos(bo)-sinpi*(s*Sin(TrueObliquity(jd))+c*Cos(TrueObliquity(jd))*Sin(tH)), n) * 180 / math.Pi
return nlo
}
func ZhanXinBo(lo, bo, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
C := pcosi(lat, h)
S := psini(lat, h)
func TopocentricBo(lo, bo, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
c := pcosi(lat, h)
s := psini(lat, h)
sinpi := Sin(0.0024427777777) / au
ra := LoToRa(jd, lo, bo)
tH := Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + lon - ra)
N := Cos(lo)*Cos(bo) - C*sinpi*Cos(tH)
nlo := math.Atan2(Sin(lo)*Cos(bo)-sinpi*(S*Sin(Sita(jd))+C*Cos(Sita(jd))*Sin(tH)), N) * 180 / math.Pi
nbo := math.Atan2(Cos(nlo)*(Sin(bo)-sinpi*(S*Cos(Sita(jd))-C*Sin(Sita(jd))*Sin(tH))), N) * 180 / math.Pi
n := Cos(lo)*Cos(bo) - c*sinpi*Cos(tH)
nlo := math.Atan2(Sin(lo)*Cos(bo)-sinpi*(s*Sin(TrueObliquity(jd))+c*Cos(TrueObliquity(jd))*Sin(tH)), n) * 180 / math.Pi
nbo := math.Atan2(Cos(nlo)*(Sin(bo)-sinpi*(s*Cos(TrueObliquity(jd))-c*Sin(TrueObliquity(jd))*Sin(tH))), n) * 180 / math.Pi
return nbo
}
-46
View File
@@ -1,46 +0,0 @@
package basic
import (
"fmt"
"testing"
)
func Test_LoBoRaDec(t *testing.T) {
jde := 2451545.0
lo, bo := RaDecToLoBo(jde, 10, 50)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
lo, bo = RaDecToLoBo(jde, 40, 80)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
lo, bo = RaDecToLoBo(jde, 90, 50)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
lo, bo = RaDecToLoBo(jde, 130, 50)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
lo, bo = RaDecToLoBo(jde, 160, 50)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
lo, bo = RaDecToLoBo(jde, 180, 50)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
lo, bo = RaDecToLoBo(jde, 210, 50)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
lo, bo = RaDecToLoBo(jde, 260, 50)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
lo, bo = RaDecToLoBo(jde, 270, 50)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
lo, bo = RaDecToLoBo(jde, 300, 50)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
lo, bo = RaDecToLoBo(jde, 350, 50)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
lo, bo = RaDecToLoBo(jde, 0, 50)
fmt.Println("LO,BO", lo, bo)
fmt.Println(LoBoToRaDec(jde, lo, bo))
}
-290
View File
@@ -1,290 +0,0 @@
package basic
import (
. "b612.me/astro/tools"
)
type cst struct {
RA float64 //赤经
DEC float64 //赤纬
}
var cstLists map[string][]cst
func initCstData() {
cstLists = make(map[string][]cst, 89)
cstLists["AND"] = []cst{cst{344.46530375, 35.1682358}, cst{344.34285125, 53.1680298}, cst{351.45289375, 53.1870041}, cst{351.4656825, 50.6870193}, cst{355.27055708333, 50.6929131}, cst{355.27607875, 48.6929169}, cst{4.1463675, 48.6949348}, cst{4.14327875, 46.6949348}, cst{14.776077083333, 46.6757545}, cst{14.7888675, 48.6757393}, cst{18.588407916667, 48.6632690}, cst{18.60590375, 50.6632347}, cst{22.40793625, 50.6478767}, cst{26.96852375, 50.6257439}, cst{26.931439583333, 47.6258430}, cst{32.62149125, 47.5927505}, cst{32.67380125, 51.0925827}, cst{39.88547875, 51.0423737}, cst{39.67934125, 37.2931557}, cst{31.87109125, 37.3470840}, cst{31.854250416667, 35.5971375}, cst{22.910835, 35.6453362}, cst{22.89742625, 33.6453705}, cst{12.44306375, 33.6818962}, cst{12.41349125, 24.4319324}, cst{14.424064583333, 24.4266243}, cst{14.414815416667, 21.6766376}, cst{3.73992125, 21.6951923}, cst{3.7406445833333, 22.6951923}, cst{2.61001625, 22.6957588}, cst{2.6128425, 28.6957588}, cst{1.60621625, 28.6960354}, cst{1.6069770833333, 32.0293655}, cst{357.82874125, 32.0285034}, cst{357.8280825, 32.7785072}, cst{354.04915791667, 32.7746468}, cst{354.04417958333, 35.1913109}}
cstLists["ANT"] = []cst{cst{141.904335, -24.5425186}, cst{141.77159875, -37.2920151}, cst{141.73406125, -40.2918739}, cst{166.45650458333, -40.4246216}, cst{166.47936291667, -35.6746559}, cst{163.95851541667, -35.6664963}, cst{163.977885, -31.8332005}, cst{160.20137875, -31.8185863}, cst{160.21289375, -29.8186131}, cst{155.18132708333, -29.7947845}, cst{155.1993375, -27.1281624}, cst{147.65928291667, -27.0835037}, cst{147.67968125, -24.5835705}}
cstLists["APS"] = []cst{cst{209.11110875, -83.1200714}, cst{276.86599791667, -82.4582748}, cst{274.19506041667, -74.9745178}, cst{273.28007708333, -67.4800797}, cst{265.77572875, -67.5711060}, cst{258.2424825, -67.6610870}, cst{258.47067875, -70.1597443}, cst{224.16644125, -70.5115433}, cst{207.46087041667, -70.6244431}, cst{207.78143375, -75.6235962}}
cstLists["AQR"] = []cst{cst{309.59884625, 0.4361772}, cst{309.5798775, 2.4360874}, cst{314.08109708333, 2.4773185}, cst{321.58347125, 2.5393796}, cst{323.58427041667, 2.5544112}, cst{323.57874875, 3.3043909}, cst{326.58021875, 3.3256676}, cst{326.58708625, 2.3256910}, cst{331.588755, 2.3576119}, cst{331.58726708333, 2.6076074}, cst{342.84221708333, 2.6622071}, cst{342.8497125, 0.6622211}, cst{342.86470375, -3.3377509}, cst{359.10221125, -3.3042023}, cst{359.10329875, -6.3042021}, cst{359.11056458333, -24.8042011}, cst{346.68096625, -24.8250446}, cst{329.77028875, -24.9040413}, cst{329.6561625, -8.4043999}, cst{321.668415, -8.4602947}, cst{321.71645125, -14.4601107}, cst{309.74390125, -14.5631361}, cst{309.68464791667, -8.5634165}}
cstLists["AQL"] = []cst{cst{280.35020875, 0.1154895}, cst{280.3262325, 2.1153460}, cst{284.57642541667, 2.1659052}, cst{284.525985, 6.4156075}, cst{281.45856875, 6.3791943}, cst{281.388045, 12.1287737}, cst{284.45626208333, 12.1651964}, cst{284.37363625, 18.6647091}, cst{286.37549375, 18.6882229}, cst{286.4054775, 16.3550682}, cst{298.92116541667, 16.4957294}, cst{298.926, 16.0790844}, cst{303.5589975, 16.1275158}, cst{303.636675, 8.8779116}, cst{306.01395625, 8.9018240}, cst{306.07910125, 2.4021468}, cst{309.5798775, 2.4360874}, cst{309.59884625, 0.4361772}, cst{309.68464791667, -8.5634165}, cst{301.69369625, -8.6430750}, cst{301.72636958333, -11.6762342}, cst{284.74405375, -11.8664360}, cst{284.64729291667, -3.8336766}, cst{280.3981875, -3.8842230}}
cstLists["ARA"] = []cst{cst{249.03468125, -60.2644577}, cst{248.57062375, -45.7670517}, cst{269.80928375, -45.5163460}, cst{272.3090175, -45.4859734}, cst{272.67225291667, -56.9837723}, cst{265.16818958333, -57.0747757}, cst{265.77572875, -67.5711060}, cst{258.2424825, -67.6610870}, cst{255.72498291667, -67.6905823}, cst{255.5423925, -65.1916428}, cst{254.28351458333, -65.2062531}, cst{254.1951375, -63.7900925}, cst{251.67632125, -63.8189964}, cst{251.53784708333, -61.2364578}, cst{249.08163, -61.2641945}}
cstLists["ARI"] = []cst{cst{31.6652475, 10.5143948}, cst{26.65573375, 10.5432396}, cst{26.744674583333, 25.6263351}, cst{30.51371125, 25.6050701}, cst{30.53061625, 27.8550186}, cst{38.07014875, 27.8047638}, cst{38.10319375, 31.2213154}, cst{42.62838, 31.1865025}, cst{52.426667916667, 31.1003609}, cst{52.2906225, 19.4343338}, cst{51.037234583333, 19.4461136}, cst{50.94641125, 10.3632069}}
cstLists["AUR"] = []cst{cst{69.4869375, 30.9218750}, cst{69.57384125, 36.2547150}, cst{72.45734375, 36.2218513}, cst{72.840285, 52.7196465}, cst{77.484762083333, 52.6655540}, cst{77.606764583333, 56.1648331}, cst{94.13108875, 55.9658089}, cst{94.05736625, 53.9662552}, cst{100.04603125, 53.8938293}, cst{99.919459583333, 49.8945885}, cst{104.40635875, 49.8410034}, cst{104.26530291667, 44.3418388}, cst{112.73412458333, 44.2435493}, cst{112.56071875, 35.2445297}, cst{100.09027625, 35.3905640}, cst{99.965657916667, 27.8913116}, cst{90.22107125, 28.0092907}, cst{90.228902916667, 28.5092430}, cst{73.212475416667, 28.7124405}, cst{73.235342916667, 30.2123089}, cst{69.47678875, 30.2552605}}
cstLists["BOO"] = []cst{cst{227.78148625, 7.5253930}, cst{204.06384875, 7.3605771}, cst{204.02893041667, 14.3604937}, cst{203.95387208333, 27.8603134}, cst{210.7888275, 27.8976517}, cst{210.77085875, 30.1475964}, cst{211.88893375, 30.1545391}, cst{211.69873208333, 47.9039383}, cst{211.58439125, 54.9035759}, cst{217.25124875, 54.9422379}, cst{229.59105458333, 55.0448647}, cst{229.65737375, 52.5451736}, cst{237.08447375, 52.6174774}, cst{237.12458541667, 51.1176796}, cst{237.36542708333, 39.6189079}, cst{232.64365208333, 39.5721130}, cst{232.74697791667, 32.5726128}, cst{229.01591875, 32.5376778}, cst{229.09951625, 25.5380573}, cst{227.60549125, 25.5246105}}
cstLists["CAE"] = []cst{cst{65.0764125, -39.7007294}, cst{64.88238625, -48.6996651}, cst{68.362247916667, -48.7384491}, cst{68.42409625, -46.2387962}, cst{73.402082916667, -46.2959023}, cst{73.482370416667, -42.7963676}, cst{75.97444375, -42.8255501}, cst{76.2549375, -27.0772038}, cst{73.75929625, -27.0479794}, cst{71.76333125, -27.0248775}, cst{71.72241875, -29.7746429}, cst{69.97665125, -29.7546597}, cst{69.862375416667, -36.7540054}, cst{65.12985, -36.7010231}}
cstLists["CAM"] = []cst{cst{94.13108875, 55.9658089}, cst{77.606764583333, 56.1648331}, cst{77.484762083333, 52.6655540}, cst{72.840285, 52.7196465}, cst{52.31308625, 52.9366074}, cst{52.381900416667, 55.4362831}, cst{49.8540225, 55.4596519}, cst{49.91349625, 57.4593849}, cst{48.90093, 57.4684982}, cst{49.3954575, 68.4662857}, cst{54.237034583333, 68.4214401}, cst{55.30874875, 77.4163132}, cst{56.726209583333, 77.4025955}, cst{57.53049, 80.3986664}, cst{80.488894583333, 80.1478500}, cst{84.536117916667, 85.1239471}, cst{127.953615, 84.6103745}, cst{130.40275041667, 86.0975418}, cst{213.0229575, 85.9308090}, cst{216.78285625, 79.4449844}, cst{203.80918958333, 79.3629303}, cst{204.15701875, 76.3638153}, cst{195.8206125, 76.3289108}, cst{174.43479625, 76.3084106}, cst{174.53158375, 79.3083420}, cst{162.81859791667, 79.3401794}, cst{163.10541625, 81.3396072}, cst{142.191195, 81.4677658}, cst{140.61547375, 72.9741364}, cst{123.08622875, 73.1383743}, cst{122.12910125, 59.6433983}, cst{107.7531975, 59.8037262}, cst{107.8515525, 61.8031464}, cst{94.40745625, 61.9641266}}
cstLists["CNC"] = []cst{cst{140.40425875, 6.4700689}, cst{122.92139125, 6.6302376}, cst{120.54834291667, 6.6549850}, cst{120.5806725, 9.6548138}, cst{118.83248875, 9.6734257}, cst{118.87160541667, 13.1732168}, cst{118.94754458333, 19.6728077}, cst{120.07012375, 19.6608200}, cst{120.17164625, 27.6602821}, cst{121.91596958333, 27.6419144}, cst{121.99323125, 33.1415138}, cst{140.645985, 32.9691162}}
cstLists["CVN"] = []cst{cst{181.59450625, 33.3039627}, cst{181.59141625, 44.3039627}, cst{182.82643375, 44.3043365}, cst{182.8185225, 52.3043365}, cst{203.74239875, 52.3598061}, cst{203.79511375, 47.8599281}, cst{211.69873208333, 47.9039383}, cst{211.88893375, 30.1545391}, cst{210.77085875, 30.1475964}, cst{210.7888275, 27.8976517}, cst{203.95387208333, 27.8603134}, cst{200.22657458333, 27.8437748}, cst{200.20774791667, 31.3437366}, cst{186.55769375, 31.3074341}, cst{186.55426041667, 33.3074303}}
cstLists["CMA"] = []cst{cst{93.215625, -11.0301533}, cst{111.97339958333, -11.2521448}, cst{111.67719875, -33.2504692}, cst{99.903859583333, -33.1128159}, cst{92.899067916667, -33.0282326}, cst{92.99256625, -27.2787991}}
cstLists["CMI"] = []cst{cst{122.84900708333, -0.3693900}, cst{109.59966625, -0.2243290}, cst{109.61691875, 1.2755718}, cst{106.86739625, 1.3074419}, cst{106.91427458333, 5.3071680}, cst{106.66432208333, 5.3100886}, cst{106.71787958333, 9.8097754}, cst{106.7482275, 12.3095980}, cst{114.24100375, 12.2238722}, cst{114.2527275, 13.2238064}, cst{118.87160541667, 13.1732168}, cst{118.83248875, 9.6734257}, cst{120.5806725, 9.6548138}, cst{120.54834291667, 6.6549850}, cst{122.92139125, 6.6302376}}
cstLists["CAP"] = []cst{cst{309.68464791667, -8.5634165}, cst{301.69369625, -8.6430750}, cst{301.72636958333, -11.6762342}, cst{301.91596958333, -27.6419144}, cst{306.89795541667, -27.5913391}, cst{321.83163625, -27.4596672}, cst{321.80777541667, -24.9597607}, cst{329.77028875, -24.9040413}, cst{329.6561625, -8.4043999}, cst{321.668415, -8.4602947}, cst{321.71645125, -14.4601107}, cst{309.74390125, -14.5631361}}
cstLists["CAR"] = []cst{cst{170.15592125, -57.1843452}, cst{166.33725625, -57.1744423}, cst{133.32365541667, -56.9739723}, cst{133.38017375, -54.9742203}, cst{127.56711875, -54.9204712}, cst{127.60929125, -53.4206772}, cst{123.32011625, -53.3782196}, cst{123.38112875, -51.1285286}, cst{120.8616975, -51.1025848}, cst{90.748902083333, -50.7545471}, cst{90.693705, -52.5042114}, cst{93.19435375, -52.5345764}, cst{93.1074, -55.0340500}, cst{98.114275416667, -55.0945587}, cst{97.995077916667, -58.0938416}, cst{103.01111708333, -58.1537018}, cst{102.70331375, -64.1518784}, cst{136.09472708333, -64.4990387}, cst{135.24368708333, -75.4954681}, cst{169.85697291667, -75.6840134}, cst{170.08481125, -64.6842651}}
cstLists["CAS"] = []cst{cst{344.34285125, 53.1680298}, cst{344.30402708333, 56.9179611}, cst{344.26912375, 59.7512321}, cst{348.85966375, 59.7646751}, cst{348.81649041667, 63.6812897}, cst{355.21757125, 63.6928787}, cst{355.19785958333, 66.6928711}, cst{6.76376375, 66.6924438}, cst{6.92291375, 77.6923447}, cst{55.30874875, 77.4163132}, cst{54.237034583333, 68.4214401}, cst{49.3954575, 68.4662857}, cst{48.90093, 57.4684982}, cst{38.762337083333, 57.5513000}, cst{38.802355416667, 59.0511551}, cst{30.795625416667, 59.1046104}, cst{30.77362375, 58.1046753}, cst{27.5952225, 58.1227188}, cst{27.53364125, 54.6228828}, cst{22.45601375, 54.6477699}, cst{22.40793625, 50.6478767}, cst{18.60590375, 50.6632347}, cst{18.588407916667, 48.6632690}, cst{14.7888675, 48.6757393}, cst{14.776077083333, 46.6757545}, cst{4.14327875, 46.6949348}, cst{4.1463675, 48.6949348}, cst{355.27607875, 48.6929169}, cst{355.27055708333, 50.6929131}, cst{351.4656825, 50.6870193}, cst{351.45289375, 53.1870041}}
cstLists["CEN"] = []cst{cst{166.47936291667, -35.6746559}, cst{166.45650458333, -40.4246216}, cst{166.33725625, -57.1744423}, cst{170.15592125, -57.1843452}, cst{170.08481125, -64.6842651}, cst{179.05736375, -64.6957855}, cst{179.07076791667, -55.6957932}, cst{194.33451125, -55.6771049}, cst{194.43838041667, -64.6769638}, cst{204.68028625, -64.6379395}, cst{220.51497458333, -64.5390244}, cst{220.23446541667, -55.5400887}, cst{214.65681625, -55.5799522}, cst{214.45026458333, -42.5806465}, cst{225.79627958333, -42.4941750}, cst{225.63076958333, -29.9948788}, cst{190.41739958333, -30.1863899}, cst{190.42719875, -33.6863785}, cst{185.38743458333, -33.6938934}, cst{185.39029625, -35.6938896}}
cstLists["CEP"] = []cst{cst{300.5732625, 59.8510780}, cst{300.48520041667, 61.8506203}, cst{306.8118675, 61.9143791}, cst{306.51738125, 67.4129562}, cst{310.33401458333, 67.4490280}, cst{309.57304041667, 75.4455261}, cst{301.87339791667, 75.3708725}, cst{300.6738, 80.3647766}, cst{313.70587375, 80.4867859}, cst{308.72097, 86.4656219}, cst{308.33135541667, 86.6306305}, cst{343.51066625, 86.8368912}, cst{339.26098791667, 88.6638870}, cst{135.83247125, 87.5689163}, cst{130.40275041667, 86.0975418}, cst{127.953615, 84.6103745}, cst{84.536117916667, 85.1239471}, cst{80.488894583333, 80.1478500}, cst{57.53049, 80.3986664}, cst{56.726209583333, 77.4025955}, cst{55.30874875, 77.4163132}, cst{6.92291375, 77.6923447}, cst{6.76376375, 66.6924438}, cst{355.19785958333, 66.6928711}, cst{355.21757125, 63.6928787}, cst{348.81649041667, 63.6812897}, cst{348.85966375, 59.7646751}, cst{344.26912375, 59.7512321}, cst{344.30402708333, 56.9179611}, cst{335.91093, 56.8825760}, cst{335.93130125, 55.6326256}, cst{333.13762625, 55.6178436}, cst{333.17467625, 53.3679428}, cst{330.63921, 53.3532715}, cst{330.60218875, 55.4364891}, cst{309.83136125, 55.2753258}, cst{309.62379458333, 61.3576965}, cst{308.66080375, 61.3486443}, cst{308.71659291667, 59.9322395}}
cstLists["CET"] = []cst{cst{6.60132875, 0.6925398}, cst{6.6037875, 2.6925383}, cst{31.61526625, 2.5978806}, cst{31.6652475, 10.5143948}, cst{50.94641125, 10.3632069}, cst{50.85298375, 0.4469725}, cst{50.836682916667, -1.3029516}, cst{41.33922125, -1.2210265}, cst{41.14875875, -23.8536034}, cst{26.46599875, -23.7562580}, cst{26.45888875, -24.8729095}, cst{359.11056458333, -24.8042011}, cst{359.10329875, -6.3042021}, cst{6.5927025, -6.3074551}}
cstLists["CHA"] = []cst{cst{111.65211458333, -82.7758865}, cst{209.11110875, -83.1200714}, cst{207.78143375, -75.6235962}, cst{169.85697291667, -75.6840134}, cst{135.24368708333, -75.4954681}, cst{114.21470375, -75.2899170}}
cstLists["CIR"] = []cst{cst{204.68028625, -64.6379395}, cst{204.70747958333, -65.6378784}, cst{207.26802291667, -65.6249542}, cst{207.46087041667, -70.6244431}, cst{224.16644125, -70.5115433}, cst{224.00363375, -68.0122070}, cst{226.55712625, -67.9909286}, cst{226.35353541667, -64.0751266}, cst{230.16657875, -64.0415649}, cst{230.05456958333, -61.4587479}, cst{232.58976458333, -61.4353065}, cst{232.5498675, -60.4354935}, cst{232.38191125, -55.4362831}, cst{228.08351125, -55.4754944}, cst{220.23446541667, -55.5400887}, cst{220.51497458333, -64.5390244}}
cstLists["COL"] = []cst{cst{75.97444375, -42.8255501}, cst{76.2549375, -27.0772038}, cst{92.99256625, -27.2787991}, cst{92.899067916667, -33.0282326}, cst{99.903859583333, -33.1128159}, cst{99.70891625, -43.1116486}, cst{90.951777083333, -43.0057793}}
cstLists["COM"] = []cst{cst{179.60453541667, 13.3040485}, cst{179.60894125, 28.3040466}, cst{181.59566375, 28.3039627}, cst{181.59450625, 33.3039627}, cst{186.55426041667, 33.3074303}, cst{186.55769375, 31.3074341}, cst{200.20774791667, 31.3437366}, cst{200.22657458333, 27.8437748}, cst{203.95387208333, 27.8603134}, cst{204.02893041667, 14.3604937}, cst{194.05906625, 14.3225088}, cst{194.0620275, 13.3225126}}
cstLists["CRA"] = []cst{cst{269.62546375, -37.0174599}, cst{289.59631958333, -36.7785645}, cst{289.76964, -45.2775650}, cst{272.3090175, -45.4859734}, cst{269.80928375, -45.5163460}}
cstLists["CRB"] = []cst{cst{229.09951625, 25.5380573}, cst{229.01591875, 32.5376778}, cst{232.74697791667, 32.5726128}, cst{232.64365208333, 39.5721130}, cst{237.36542708333, 39.6189079}, cst{246.07194875, 39.7117195}, cst{246.2798025, 26.7128716}, cst{243.78670625, 26.6855240}, cst{243.8001825, 25.6855946}, cst{241.80573458333, 25.6641407}}
cstLists["CRV"] = []cst{cst{194.1330525, -11.6773882}, cst{179.09676, -11.6957970}, cst{179.09131041667, -25.1957951}, cst{190.404525, -25.1864014}, cst{190.3985025, -22.6864090}, cst{194.16687, -22.6773415}}
cstLists["CRT"] = []cst{cst{162.82713875, -6.6621790}, cst{162.80791375, -11.6621428}, cst{162.77554041667, -19.6620827}, cst{164.03058625, -19.6666222}, cst{164.00808291667, -25.1665821}, cst{179.09131041667, -25.1957951}, cst{179.09676, -11.6957970}, cst{179.09860625, -6.6957974}, cst{174.34229875, -6.6916924}}
cstLists["CRU"] = []cst{cst{179.07076791667, -55.6957932}, cst{179.05736375, -64.6957855}, cst{194.43838041667, -64.6769638}, cst{194.33451125, -55.6771049}}
cstLists["CYG"] = []cst{cst{290.13264625, 27.7324085}, cst{290.0952525, 30.2321968}, cst{291.5987775, 30.2493153}, cst{291.49260458333, 36.7487144}, cst{292.11965541667, 36.7558022}, cst{291.9835275, 43.7550354}, cst{288.47030708333, 43.7149391}, cst{288.37552041667, 47.7143936}, cst{287.1206475, 47.6998672}, cst{286.87645958333, 55.6984482}, cst{291.90459291667, 55.7560043}, cst{291.80955, 58.2554703}, cst{297.10055375, 58.3138733}, cst{297.03924125, 59.8135414}, cst{300.5732625, 59.8510780}, cst{308.71659291667, 59.9322395}, cst{308.66080375, 61.3486443}, cst{309.62379458333, 61.3576965}, cst{309.83136125, 55.2753258}, cst{330.60218875, 55.4364891}, cst{330.63921, 53.3532715}, cst{330.76266291667, 44.6036453}, cst{329.87860625, 44.5982513}, cst{329.88163958333, 44.3482628}, cst{329.37664041667, 44.3451195}, cst{329.4610125, 36.5953827}, cst{327.319965, 36.5815468}, cst{327.39518125, 28.5817947}, cst{322.62016375, 28.5480537}, cst{315.08391458333, 28.4871883}, cst{315.07258375, 29.4871387}, cst{296.25094375, 29.3010578}, cst{296.27220125, 27.8011742}}
cstLists["DEL"] = []cst{cst{309.5798775, 2.4360874}, cst{306.07910125, 2.4021468}, cst{306.01395625, 8.9018240}, cst{303.636675, 8.8779116}, cst{303.5589975, 16.1275158}, cst{305.18694875, 16.1439629}, cst{305.13404875, 20.8936996}, cst{309.89693708333, 20.9399471}, cst{309.907665, 19.9399967}, cst{317.17878291667, 20.0046406}, cst{317.24836375, 12.3382607}, cst{314.61859625, 12.3157644}, cst{314.67109625, 6.4826641}, cst{314.045505, 6.4771614}, cst{314.08109708333, 2.4773185}}
cstLists["DOR"] = []cst{cst{58.318787916667, -52.7968445}, cst{60.79789625, -52.8228111}, cst{60.69291875, -56.1555862}, cst{65.6504625, -56.2093849}, cst{65.55459, -58.7088547}, cst{69.274534583333, -58.7506638}, cst{68.79401875, -67.2479248}, cst{68.58152375, -69.7467194}, cst{98.454422916667, -70.1041336}, cst{98.93724875, -64.1070251}, cst{90.173642916667, -64.0010529}, cst{90.34506125, -61.0020981}, cst{82.85761375, -60.9112892}, cst{83.01880375, -57.4122620}, cst{75.547742916667, -57.3230400}, cst{75.6770175, -53.8238029}, cst{68.217745416667, -53.7376366}, cst{68.362247916667, -48.7384491}, cst{64.88238625, -48.6996651}, cst{62.149907916667, -48.6699715}, cst{62.098639583333, -50.6697006}, cst{58.37716625, -50.6304779}}
cstLists["DRA"] = []cst{cst{140.61547375, 72.9741364}, cst{142.191195, 81.4677658}, cst{163.10541625, 81.3396072}, cst{162.81859791667, 79.3401794}, cst{174.53158375, 79.3083420}, cst{174.43479625, 76.3084106}, cst{195.8206125, 76.3289108}, cst{196.09747375, 69.3293610}, cst{210.65081125, 69.3991165}, cst{210.82055541667, 65.3996506}, cst{235.32956541667, 65.6023483}, cst{235.05063, 69.6009445}, cst{247.8410625, 69.7383041}, cst{247.2207075, 74.7347870}, cst{261.53663708333, 74.9033127}, cst{260.21790458333, 79.8953476}, cst{267.65602041667, 79.9857483}, cst{261.72223041667, 85.9495697}, cst{308.72097, 86.4656219}, cst{313.70587375, 80.4867859}, cst{300.6738, 80.3647766}, cst{301.87339791667, 75.3708725}, cst{309.57304041667, 75.4455261}, cst{310.33401458333, 67.4490280}, cst{306.51738125, 67.4129562}, cst{306.8118675, 61.9143791}, cst{300.48520041667, 61.8506203}, cst{300.5732625, 59.8510780}, cst{297.03924125, 59.8135414}, cst{297.10055375, 58.3138733}, cst{291.80955, 58.2554703}, cst{291.90459291667, 55.7560043}, cst{286.87645958333, 55.6984482}, cst{287.1206475, 47.6998672}, cst{274.34237541667, 47.5476036}, cst{274.25768875, 50.5470886}, cst{255.7863525, 50.3244438}, cst{255.75682625, 51.3242683}, cst{237.12458541667, 51.1176796}, cst{237.08447375, 52.6174774}, cst{229.65737375, 52.5451736}, cst{229.59105458333, 55.0448647}, cst{217.25124875, 54.9422379}, cst{217.04525375, 62.4414825}, cst{203.57364125, 62.3593979}, cst{203.55053875, 63.3593445}, cst{181.58155958333, 63.3039627}, cst{181.57925541667, 65.8039627}, cst{171.84934625, 65.8126068}, cst{171.96136958333, 72.8125000}}
cstLists["EQU"] = []cst{cst{314.08109708333, 2.4773185}, cst{314.045505, 6.4771614}, cst{314.67109625, 6.4826641}, cst{314.61859625, 12.3157644}, cst{317.24836375, 12.3382607}, cst{318.25026458333, 12.3465548}, cst{318.244515, 13.0132008}, cst{321.50110208333, 13.0390635}, cst{321.58347125, 2.5393796}}
cstLists["ERI"] = []cst{cst{55.352905416667, 0.4037257}, cst{70.852360416667, 0.2375014}, cst{71.60231375, 0.2289162}, cst{71.55635875, -3.7708201}, cst{77.804395416667, -3.8437285}, cst{77.72003125, -10.8432293}, cst{75.22175125, -10.8138046}, cst{75.178714583333, -14.3135529}, cst{73.929797916667, -14.2989721}, cst{73.75929625, -27.0479794}, cst{71.76333125, -27.0248775}, cst{71.72241875, -29.7746429}, cst{69.97665125, -29.7546597}, cst{69.862375416667, -36.7540054}, cst{65.12985, -36.7010231}, cst{65.0764125, -39.7007294}, cst{59.105884583333, -39.6368256}, cst{59.031364583333, -43.6364403}, cst{52.3267725, -43.5694046}, cst{52.2890025, -45.5692215}, cst{46.09077125, -45.5124779}, cst{46.03451375, -48.5122337}, cst{41.08530875, -48.4710045}, cst{41.04769375, -50.4708595}, cst{37.34121, -50.4425697}, cst{37.28334625, -53.4423561}, cst{33.58414375, -53.4164696}, cst{33.489424583333, -57.9161568}, cst{21.20622875, -57.8484154}, cst{21.2732625, -52.8485603}, cst{24.967354583333, -52.8658562}, cst{24.9938475, -50.8659210}, cst{28.693257083333, -50.8859215}, cst{28.738322916667, -47.5527229}, cst{36.1529475, -47.6004944}, cst{36.26401375, -39.4342155}, cst{46.187260416667, -39.5128975}, cst{46.193322083333, -39.0962563}, cst{53.64428375, -39.1650963}, cst{53.699605416667, -35.5820351}, cst{57.430512083333, -35.6192436}, cst{57.58890125, -24.0033779}, cst{41.14875875, -23.8536034}, cst{41.33922125, -1.2210265}, cst{50.836682916667, -1.3029516}, cst{55.335582083333, -1.3461887}}
cstLists["FOR"] = []cst{cst{26.46599875, -23.7562580}, cst{41.14875875, -23.8536034}, cst{57.58890125, -24.0033779}, cst{57.430512083333, -35.6192436}, cst{53.699605416667, -35.5820351}, cst{53.64428375, -39.1650963}, cst{46.193322083333, -39.0962563}, cst{46.187260416667, -39.5128975}, cst{36.26401375, -39.4342155}, cst{26.350727916667, -39.3726234}, cst{26.45888875, -24.8729095}}
cstLists["GEM"] = []cst{cst{96.37276375, 11.9332972}, cst{96.4439175, 17.4328651}, cst{95.069575416667, 17.4495068}, cst{95.1241275, 21.4491768}, cst{90.125155416667, 21.5098724}, cst{90.1440375, 22.8430862}, cst{90.22107125, 28.0092907}, cst{99.965657916667, 27.8913116}, cst{100.09027625, 35.3905640}, cst{112.56071875, 35.2445297}, cst{118.28970875, 35.1810532}, cst{118.25808, 33.1812286}, cst{121.99323125, 33.1415138}, cst{121.91596958333, 27.6419144}, cst{120.17164625, 27.6602821}, cst{120.07012375, 19.6608200}, cst{118.94754458333, 19.6728077}, cst{118.87160541667, 13.1732168}, cst{114.2527275, 13.2238064}, cst{114.24100375, 12.2238722}, cst{106.7482275, 12.3095980}, cst{106.71787958333, 9.8097754}, cst{105.71845958333, 9.8214874}, cst{105.742815, 11.8213453}}
cstLists["GRU"] = []cst{cst{321.92805291667, -36.4592972}, cst{322.04232125, -44.9588585}, cst{322.11736625, -49.4585724}, cst{331.998825, -49.3911743}, cst{332.113695, -56.3908348}, cst{351.76852208333, -56.3126869}, cst{351.69270458333, -39.3127594}, cst{351.6833775, -36.3127670}, cst{346.72751375, -36.3249741}}
cstLists["HER"] = []cst{cst{245.558595, 3.7033811}, cst{242.80966791667, 3.6735139}, cst{242.67663, 15.6728001}, cst{240.18107375, 15.6463346}, cst{240.1110075, 21.6459675}, cst{241.85657541667, 21.6644115}, cst{241.80573458333, 25.6641407}, cst{243.8001825, 25.6855946}, cst{243.78670625, 26.6855240}, cst{246.2798025, 26.7128716}, cst{246.07194875, 39.7117195}, cst{237.36542708333, 39.6189079}, cst{237.12458541667, 51.1176796}, cst{255.75682625, 51.3242683}, cst{255.7863525, 50.3244438}, cst{274.25768875, 50.5470886}, cst{274.34237541667, 47.5476036}, cst{273.46687375, 47.5369873}, cst{273.82438625, 30.0391560}, cst{276.70077291667, 30.0739765}, cst{276.76288625, 26.0743504}, cst{284.2698675, 26.1640968}, cst{284.27716208333, 25.6641407}, cst{284.33913291667, 21.2478352}, cst{284.37363625, 18.6647091}, cst{284.45626208333, 12.1651964}, cst{281.388045, 12.1287737}, cst{275.20308458333, 12.0543308}, cst{275.17327375, 14.3874788}, cst{260.17687791667, 14.2060347}, cst{260.19584708333, 12.7061481}, cst{252.7014825, 12.6179380}, cst{252.80590958333, 3.7852108}}
cstLists["HOR"] = []cst{cst{65.0764125, -39.7007294}, cst{64.88238625, -48.6996651}, cst{62.149907916667, -48.6699715}, cst{62.098639583333, -50.6697006}, cst{58.37716625, -50.6304779}, cst{58.318787916667, -52.7968445}, cst{53.365002083333, -52.7470779}, cst{53.23681625, -57.0797844}, cst{48.79113125, -57.0377846}, cst{48.362689583333, -67.0358200}, cst{33.202360416667, -66.9151917}, cst{33.489424583333, -57.9161568}, cst{33.58414375, -53.4164696}, cst{37.28334625, -53.4423561}, cst{37.34121, -50.4425697}, cst{41.04769375, -50.4708595}, cst{41.08530875, -48.4710045}, cst{46.03451375, -48.5122337}, cst{46.09077125, -45.5124779}, cst{52.2890025, -45.5692215}, cst{52.3267725, -43.5694046}, cst{59.031364583333, -43.6364403}, cst{59.105884583333, -39.6368256}}
cstLists["HYA"] = []cst{cst{122.84900708333, -0.3693900}, cst{122.92139125, 6.6302376}, cst{140.40425875, 6.4700689}, cst{145.39841708333, 6.4327669}, cst{145.34890625, -0.5670585}, cst{145.27027208333, -11.5667810}, cst{162.80791375, -11.6621428}, cst{162.77554041667, -19.6620827}, cst{164.03058625, -19.6666222}, cst{164.00808291667, -25.1665821}, cst{179.09131041667, -25.1957951}, cst{190.404525, -25.1864014}, cst{190.3985025, -22.6864090}, cst{194.16687, -22.6773415}, cst{215.51309125, -22.5727749}, cst{215.53369041667, -25.0727024}, cst{225.57655375, -24.9951096}, cst{225.63076958333, -29.9948788}, cst{190.41739958333, -30.1863899}, cst{190.42719875, -33.6863785}, cst{185.38743458333, -33.6938934}, cst{185.39029625, -35.6938896}, cst{166.47936291667, -35.6746559}, cst{163.95851541667, -35.6664963}, cst{163.977885, -31.8332005}, cst{160.20137875, -31.8185863}, cst{160.21289375, -29.8186131}, cst{155.18132708333, -29.7947845}, cst{155.1993375, -27.1281624}, cst{147.65928291667, -27.0835037}, cst{147.67968125, -24.5835705}, cst{141.904335, -24.5425186}, cst{137.63677625, -24.5086308}, cst{137.68497, -19.5088310}, cst{130.1635125, -19.4423733}, cst{130.18434, -17.4424706}, cst{126.92709458333, -17.4112568}, cst{126.98977958333, -11.4115648}, cst{122.73417875, -11.3687992}}
cstLists["HYI"] = []cst{cst{68.79401875, -67.2479248}, cst{68.58152375, -69.7467194}, cst{67.957485, -74.7431641}, cst{52.075782083333, -74.5741272}, cst{50.091655416667, -82.0644531}, cst{1.53339125, -81.8039551}, cst{1.5662970833333, -74.3039627}, cst{12.3324375, -74.3185730}, cst{12.295414583333, -75.3185272}, cst{20.654050416667, -75.3472214}, cst{21.20622875, -57.8484154}, cst{33.489424583333, -57.9161568}, cst{33.202360416667, -66.9151917}, cst{48.362689583333, -67.0358200}}
cstLists["IND"] = []cst{cst{323.1847575, -74.4544678}, cst{351.99783291667, -74.3124619}, cst{351.86139125, -66.8125992}, cst{332.3985675, -66.8899918}, cst{332.113695, -56.3908348}, cst{331.998825, -49.3911743}, cst{322.11736625, -49.4585724}, cst{322.04232125, -44.9588585}, cst{307.169295, -45.0900002}, cst{307.45880125, -56.5885773}, cst{307.56480125, -59.5880547}, cst{322.34865125, -59.4576836}}
cstLists["LAC"] = []cst{cst{329.4610125, 36.5953827}, cst{329.37664041667, 44.3451195}, cst{329.88163958333, 44.3482628}, cst{329.87860625, 44.5982513}, cst{330.76266291667, 44.6036453}, cst{330.63921, 53.3532715}, cst{333.17467625, 53.3679428}, cst{333.13762625, 55.6178436}, cst{335.93130125, 55.6326256}, cst{335.91093, 56.8825760}, cst{344.30402708333, 56.9179611}, cst{344.34285125, 53.1680298}, cst{344.46530375, 35.1682358}, cst{343.70919291667, 35.1656151}, cst{343.70653208333, 35.6656113}, cst{331.35955791667, 35.6069336}, cst{331.35046041667, 36.6069069}}
cstLists["LEO"] = []cst{cst{162.8497125, -0.6622211}, cst{162.87601958333, 6.3377299}, cst{145.39841708333, 6.4327669}, cst{140.40425875, 6.4700689}, cst{140.645985, 32.9691162}, cst{150.08438541667, 32.9022789}, cst{150.04234375, 27.9024086}, cst{159.23840125, 27.8529167}, cst{159.2108775, 22.8529778}, cst{162.94253875, 22.8376045}, cst{162.95149375, 24.8375893}, cst{166.6809375, 24.8250446}, cst{166.69399791667, 28.3250256}, cst{179.60894125, 28.3040466}, cst{179.60453541667, 13.3040485}, cst{179.60373458333, 10.3040485}, cst{174.36568791667, 10.3082914}, cst{174.35052458333, -0.6916979}, cst{174.34229875, -6.6916924}, cst{162.82713875, -6.6621790}}
cstLists["LMI"] = []cst{cst{140.645985, 32.9691162}, cst{140.72163125, 39.2188187}, cst{145.6819725, 39.1817665}, cst{145.70923791667, 41.4316750}, cst{154.37822375, 41.3773613}, cst{154.3594125, 39.3774109}, cst{163.52316875, 39.3356133}, cst{163.48940875, 33.3356781}, cst{166.71422541667, 33.3249931}, cst{166.69399791667, 28.3250256}, cst{166.6809375, 24.8250446}, cst{162.95149375, 24.8375893}, cst{162.94253875, 22.8376045}, cst{159.2108775, 22.8529778}, cst{159.23840125, 27.8529167}, cst{150.04234375, 27.9024086}, cst{150.08438541667, 32.9022789}}
cstLists["LEP"] = []cst{cst{73.75929625, -27.0479794}, cst{76.2549375, -27.0772038}, cst{92.99256625, -27.2787991}, cst{93.215625, -11.0301533}, cst{88.965790416667, -10.9785318}, cst{77.72003125, -10.8432293}, cst{75.22175125, -10.8138046}, cst{75.178714583333, -14.3135529}, cst{73.929797916667, -14.2989721}}
cstLists["LIB"] = []cst{cst{227.85301208333, -0.4742887}, cst{221.6030925, -0.5269387}, cst{221.66710791667, -8.5266848}, cst{215.40850625, -8.5731344}, cst{215.51309125, -22.5727749}, cst{215.53369041667, -25.0727024}, cst{225.57655375, -24.9951096}, cst{225.63076958333, -29.9948788}, cst{236.92998, -29.8896160}, cst{236.81306375, -20.3902016}, cst{240.57177625, -20.3516178}, cst{240.43727875, -8.3523235}, cst{240.38695375, -3.6025870}, cst{227.88195125, -3.7241600}}
cstLists["LUP"] = []cst{cst{214.65681625, -55.5799522}, cst{220.23446541667, -55.5400887}, cst{228.08351125, -55.4754944}, cst{228.05671625, -54.4756165}, cst{232.35337208333, -54.4364166}, cst{232.20724625, -48.4371071}, cst{237.24728125, -48.3880234}, cst{237.12458541667, -42.3886375}, cst{242.15280625, -42.3366776}, cst{241.94769875, -29.8377628}, cst{236.92998, -29.8896160}, cst{225.63076958333, -29.9948788}, cst{225.79627958333, -42.4941750}, cst{214.45026458333, -42.5806465}}
cstLists["LYN"] = []cst{cst{112.56071875, 35.2445297}, cst{112.73412458333, 44.2435493}, cst{104.26530291667, 44.3418388}, cst{104.40635875, 49.8410034}, cst{99.919459583333, 49.8945885}, cst{100.04603125, 53.8938293}, cst{94.05736625, 53.9662552}, cst{94.13108875, 55.9658089}, cst{94.40745625, 61.9641266}, cst{107.8515525, 61.8031464}, cst{107.7531975, 59.8037262}, cst{122.12910125, 59.6433983}, cst{128.79913375, 59.5759888}, cst{128.44010375, 46.5777283}, cst{139.59071125, 46.4782791}, cst{139.51249041667, 41.4785957}, cst{145.70923791667, 41.4316750}, cst{145.6819725, 39.1817665}, cst{140.72163125, 39.2188187}, cst{140.645985, 32.9691162}, cst{121.99323125, 33.1415138}, cst{118.25808, 33.1812286}, cst{118.28970875, 35.1810532}}
cstLists["LYR"] = []cst{cst{284.27716208333, 25.6641407}, cst{284.2698675, 26.1640968}, cst{276.76288625, 26.0743504}, cst{276.70077291667, 30.0739765}, cst{273.82438625, 30.0391560}, cst{273.46687375, 47.5369873}, cst{274.34237541667, 47.5476036}, cst{287.1206475, 47.6998672}, cst{288.37552041667, 47.7143936}, cst{288.47030708333, 43.7149391}, cst{291.9835275, 43.7550354}, cst{292.11965541667, 36.7558022}, cst{291.49260458333, 36.7487144}, cst{291.5987775, 30.2493153}, cst{290.0952525, 30.2321968}, cst{290.13264625, 27.7324085}, cst{290.16131375, 25.7325745}}
cstLists["MEN"] = []cst{cst{109.01970875, -85.2614441}, cst{48.23292, -84.5553818}, cst{50.091655416667, -82.0644531}, cst{52.075782083333, -74.5741272}, cst{67.957485, -74.7431641}, cst{68.58152375, -69.7467194}, cst{98.454422916667, -70.1041336}, cst{97.770709583333, -75.1000366}, cst{114.21470375, -75.2899170}, cst{111.65211458333, -82.7758865}}
cstLists["MIC"] = []cst{cst{306.89795541667, -27.5913391}, cst{321.83163625, -27.4596672}, cst{321.92805291667, -36.4592972}, cst{322.04232125, -44.9588585}, cst{307.169295, -45.0900002}}
cstLists["MON"] = []cst{cst{95.225680416667, -0.0537102}, cst{95.34803125, 9.9455481}, cst{96.347665416667, 9.9334478}, cst{96.37276375, 11.9332972}, cst{105.742815, 11.8213453}, cst{105.71845958333, 9.8214874}, cst{106.71787958333, 9.8097754}, cst{106.66432208333, 5.3100886}, cst{106.91427458333, 5.3071680}, cst{106.86739625, 1.3074419}, cst{109.61691875, 1.2755718}, cst{109.59966625, -0.2243290}, cst{122.84900708333, -0.3693900}, cst{122.73417875, -11.3687992}, cst{111.97339958333, -11.2521448}, cst{93.215625, -11.0301533}, cst{88.965790416667, -10.9785318}, cst{89.052372083333, -3.9790573}, cst{95.177142083333, -4.0534163}}
cstLists["MUS"] = []cst{cst{170.08481125, -64.6842651}, cst{169.85697291667, -75.6840134}, cst{207.78143375, -75.6235962}, cst{207.46087041667, -70.6244431}, cst{207.26802291667, -65.6249542}, cst{204.70747958333, -65.6378784}, cst{204.68028625, -64.6379395}, cst{194.43838041667, -64.6769638}, cst{179.05736375, -64.6957855}}
cstLists["NOR"] = []cst{cst{232.5498675, -60.4354935}, cst{249.03468125, -60.2644577}, cst{248.57062375, -45.7670517}, cst{248.4947775, -42.2674789}, cst{242.15280625, -42.3366776}, cst{237.12458541667, -42.3886375}, cst{237.24728125, -48.3880234}, cst{232.20724625, -48.4371071}, cst{232.35337208333, -54.4364166}, cst{228.05671625, -54.4756165}, cst{228.08351125, -55.4754944}, cst{232.38191125, -55.4362831}}
cstLists["OCT"] = []cst{cst{0.80064625, -89.3039017}, cst{1.53339125, -81.8039551}, cst{50.091655416667, -82.0644531}, cst{48.23292, -84.5553818}, cst{109.01970875, -85.2614441}, cst{111.65211458333, -82.7758865}, cst{209.11110875, -83.1200714}, cst{276.86599791667, -82.4582748}, cst{274.19506041667, -74.9745178}, cst{323.1847575, -74.4544678}, cst{351.99783291667, -74.3124619}, cst{1.56630625, -74.3039627}, cst{0.80064625, -89.3039017}, cst{0.80065208333333, -89.3038940}}
cstLists["OPH"] = []cst{cst{245.6026275, -0.2963768}, cst{245.558595, 3.7033811}, cst{252.80590958333, 3.7852108}, cst{252.7014825, 12.6179380}, cst{260.19584708333, 12.7061481}, cst{260.17687791667, 14.2060347}, cst{275.17327375, 14.3874788}, cst{275.20308458333, 12.0543308}, cst{281.388045, 12.1287737}, cst{281.45856875, 6.3791943}, cst{275.27461041667, 6.3047633}, cst{275.29601125, 4.5548930}, cst{277.87113125, 4.5860157}, cst{277.88929958333, 3.0861249}, cst{275.31423625, 3.0550034}, cst{275.35059875, 0.0552235}, cst{269.10103791667, -0.0206471}, cst{269.14970375, -4.0203514}, cst{271.14967375, -3.9960551}, cst{271.22371625, -9.9956055}, cst{266.72375708333, -10.0502338}, cst{266.7447, -11.7167768}, cst{265.49440375, -11.7319136}, cst{265.47349041667, -10.0653696}, cst{259.22206958333, -10.1404381}, cst{259.29742791667, -16.1399899}, cst{265.80019041667, -16.0618820}, cst{266.00183541667, -30.0606632}, cst{253.23534875, -30.2123089}, cst{253.15567041667, -24.7960968}, cst{245.89144625, -24.8781185}, cst{245.82298375, -19.5451660}, cst{247.45067541667, -19.5271549}, cst{247.43823, -18.5272255}, cst{245.81068041667, -18.5452347}, cst{245.69120375, -8.2958899}, cst{240.43727875, -8.3523235}, cst{240.38695375, -3.6025870}, cst{245.63838875, -3.5461800}}
cstLists["ORI"] = []cst{cst{70.852360416667, 0.2375014}, cst{71.03402875, 15.7364635}, cst{76.28892625, 15.6755352}, cst{76.29527875, 16.1754990}, cst{81.7987125, 16.1101055}, cst{81.7922325, 15.6101446}, cst{85.79364625, 15.5619202}, cst{85.755057083333, 12.5621548}, cst{88.255369583333, 12.5318508}, cst{88.327167083333, 18.0314159}, cst{87.326982083333, 18.0435486}, cst{87.39379375, 22.8764725}, cst{90.1440375, 22.8430862}, cst{90.125155416667, 21.5098724}, cst{95.1241275, 21.4491768}, cst{95.069575416667, 17.4495068}, cst{96.4439175, 17.4328651}, cst{96.37276375, 11.9332972}, cst{96.347665416667, 9.9334478}, cst{95.34803125, 9.9455481}, cst{95.225680416667, -0.0537102}, cst{95.177142083333, -4.0534163}, cst{89.052372083333, -3.9790573}, cst{88.965790416667, -10.9785318}, cst{77.72003125, -10.8432293}, cst{77.804395416667, -3.8437285}, cst{71.55635875, -3.7708201}, cst{71.60231375, 0.2289162}}
cstLists["PAV"] = []cst{cst{274.19506041667, -74.9745178}, cst{323.1847575, -74.4544678}, cst{322.34865125, -59.4576836}, cst{307.56480125, -59.5880547}, cst{307.45880125, -56.5885773}, cst{272.67225291667, -56.9837723}, cst{265.16818958333, -57.0747757}, cst{265.77572875, -67.5711060}, cst{273.28007708333, -67.4800797}}
cstLists["PEG"] = []cst{cst{321.58347125, 2.5393796}, cst{321.50110208333, 13.0390635}, cst{318.244515, 13.0132008}, cst{318.25026458333, 12.3465548}, cst{317.24836375, 12.3382607}, cst{317.17878291667, 20.0046406}, cst{320.18840875, 20.0290813}, cst{320.15170041667, 24.0289364}, cst{322.66201958333, 24.0482101}, cst{322.62016375, 28.5480537}, cst{327.39518125, 28.5817947}, cst{327.319965, 36.5815468}, cst{329.4610125, 36.5953827}, cst{331.35046041667, 36.6069069}, cst{331.35955791667, 35.6069336}, cst{343.70653208333, 35.6656113}, cst{343.70919291667, 35.1656151}, cst{344.46530375, 35.1682358}, cst{354.04417958333, 35.1913109}, cst{354.04915791667, 32.7746468}, cst{357.8280825, 32.7785072}, cst{357.82874125, 32.0285034}, cst{1.6069770833333, 32.0293655}, cst{1.60621625, 28.6960354}, cst{2.6128425, 28.6957588}, cst{2.61001625, 22.6957588}, cst{3.7406445833333, 22.6951923}, cst{3.73992125, 21.6951923}, cst{3.7341179166667, 13.1951942}, cst{1.6031745833333, 13.1960354}, cst{1.6027304166667, 10.6960354}, cst{359.09711875, 10.6957970}, cst{359.09803375, 8.1957970}, cst{342.82141625, 8.1621685}, cst{342.84221708333, 2.6622071}, cst{331.58726708333, 2.6076074}, cst{331.588755, 2.3576119}, cst{326.58708625, 2.3256910}, cst{326.58021875, 3.3256676}, cst{323.57874875, 3.3043909}, cst{323.58427041667, 2.5544112}}
cstLists["PER"] = []cst{cst{42.62838, 31.1865025}, cst{42.666467916667, 34.5196762}, cst{40.402382916667, 34.5375137}, cst{40.43465875, 37.2873878}, cst{39.67934125, 37.2931557}, cst{39.88547875, 51.0423737}, cst{32.67380125, 51.0925827}, cst{32.62149125, 47.5927505}, cst{26.931439583333, 47.6258430}, cst{26.96852375, 50.6257439}, cst{22.40793625, 50.6478767}, cst{22.45601375, 54.6477699}, cst{27.53364125, 54.6228828}, cst{27.5952225, 58.1227188}, cst{30.77362375, 58.1046753}, cst{30.795625416667, 59.1046104}, cst{38.802355416667, 59.0511551}, cst{38.762337083333, 57.5513000}, cst{48.90093, 57.4684982}, cst{49.91349625, 57.4593849}, cst{49.8540225, 55.4596519}, cst{52.381900416667, 55.4362831}, cst{52.31308625, 52.9366074}, cst{72.840285, 52.7196465}, cst{72.45734375, 36.2218513}, cst{69.57384125, 36.2547150}, cst{69.4869375, 30.9218750}, cst{52.426667916667, 31.1003609}}
cstLists["PHE"] = []cst{cst{351.69270458333, -39.3127594}, cst{351.76852208333, -56.3126869}, cst{351.77839375, -57.8126793}, cst{21.20622875, -57.8484154}, cst{21.2732625, -52.8485603}, cst{24.967354583333, -52.8658562}, cst{24.9938475, -50.8659210}, cst{28.693257083333, -50.8859215}, cst{28.738322916667, -47.5527229}, cst{36.1529475, -47.6004944}, cst{36.26401375, -39.4342155}, cst{26.350727916667, -39.3726234}}
cstLists["PIC"] = []cst{cst{90.951777083333, -43.0057793}, cst{75.97444375, -42.8255501}, cst{73.482370416667, -42.7963676}, cst{73.402082916667, -46.2959023}, cst{68.42409625, -46.2387962}, cst{68.362247916667, -48.7384491}, cst{68.217745416667, -53.7376366}, cst{75.6770175, -53.8238029}, cst{75.547742916667, -57.3230400}, cst{83.01880375, -57.4122620}, cst{82.85761375, -60.9112892}, cst{90.34506125, -61.0020981}, cst{90.173642916667, -64.0010529}, cst{98.93724875, -64.1070251}, cst{102.70331375, -64.1518784}, cst{103.01111708333, -58.1537018}, cst{97.995077916667, -58.0938416}, cst{98.114275416667, -55.0945587}, cst{93.1074, -55.0340500}, cst{93.19435375, -52.5345764}, cst{90.693705, -52.5042114}, cst{90.748902083333, -50.7545471}}
cstLists["PSC"] = []cst{cst{342.8497125, 0.6622211}, cst{342.84221708333, 2.6622071}, cst{342.82141625, 8.1621685}, cst{359.09803375, 8.1957970}, cst{359.09711875, 10.6957970}, cst{1.6027304166667, 10.6960354}, cst{1.6031745833333, 13.1960354}, cst{3.7341179166667, 13.1951942}, cst{3.73992125, 21.6951923}, cst{14.414815416667, 21.6766376}, cst{14.424064583333, 24.4266243}, cst{12.41349125, 24.4319324}, cst{12.44306375, 33.6818962}, cst{22.89742625, 33.6453705}, cst{22.86642, 28.6454391}, cst{26.76471, 28.6262817}, cst{26.744674583333, 25.6263351}, cst{26.65573375, 10.5432396}, cst{31.6652475, 10.5143948}, cst{31.61526625, 2.5978806}, cst{6.6037875, 2.6925383}, cst{6.60132875, 0.6925398}, cst{6.5927025, -6.3074551}, cst{359.10329875, -6.3042021}, cst{359.10221125, -3.3042023}, cst{342.86470375, -3.3377509}}
cstLists["PSA"] = []cst{cst{346.68096625, -24.8250446}, cst{329.77028875, -24.9040413}, cst{321.80777541667, -24.9597607}, cst{321.83163625, -27.4596672}, cst{321.92805291667, -36.4592972}, cst{346.72751375, -36.3249741}}
cstLists["PUP"] = []cst{cst{111.97339958333, -11.2521448}, cst{111.67719875, -33.2504692}, cst{99.903859583333, -33.1128159}, cst{99.70891625, -43.1116486}, cst{90.951777083333, -43.0057793}, cst{90.748902083333, -50.7545471}, cst{120.8616975, -51.1025848}, cst{121.03827875, -43.3535042}, cst{126.57231291667, -43.4095192}, cst{126.67779875, -37.1600380}, cst{126.92709458333, -17.4112568}, cst{126.98977958333, -11.4115648}, cst{122.73417875, -11.3687992}}
cstLists["PYX"] = []cst{cst{126.92709458333, -17.4112568}, cst{130.18434, -17.4424706}, cst{130.1635125, -19.4423733}, cst{137.68497, -19.5088310}, cst{137.63677625, -24.5086308}, cst{141.904335, -24.5425186}, cst{141.77159875, -37.2920151}, cst{126.67779875, -37.1600380}}
cstLists["RET"] = []cst{cst{48.362689583333, -67.0358200}, cst{68.79401875, -67.2479248}, cst{69.274534583333, -58.7506638}, cst{65.55459, -58.7088547}, cst{65.6504625, -56.2093849}, cst{60.69291875, -56.1555862}, cst{60.79789625, -52.8228111}, cst{58.318787916667, -52.7968445}, cst{53.365002083333, -52.7470779}, cst{53.23681625, -57.0797844}, cst{48.79113125, -57.0377846}}
cstLists["SGE"] = []cst{cst{284.37363625, 18.6647091}, cst{284.33913291667, 21.2478352}, cst{290.09631125, 21.3148155}, cst{290.12128791667, 19.3982983}, cst{298.88568875, 19.4955387}, cst{298.860255, 21.5787334}, cst{305.12540875, 21.6436558}, cst{305.13404875, 20.8936996}, cst{305.18694875, 16.1439629}, cst{303.5589975, 16.1275158}, cst{298.926, 16.0790844}, cst{298.92116541667, 16.4957294}, cst{286.4054775, 16.3550682}, cst{286.37549375, 18.6882229}}
cstLists["SGR"] = []cst{cst{284.74405375, -11.8664360}, cst{284.79372, -15.8328123}, cst{275.54952625, -15.9435720}, cst{265.80019041667, -16.0618820}, cst{266.00183541667, -30.0606632}, cst{269.50281125, -30.0182076}, cst{269.62546375, -37.0174599}, cst{289.59631958333, -36.7785645}, cst{289.76964, -45.2775650}, cst{307.169295, -45.0900002}, cst{306.89795541667, -27.5913391}, cst{301.91596958333, -27.6419144}, cst{301.72636958333, -11.6762342}}
cstLists["SCO"] = []cst{cst{240.43727875, -8.3523235}, cst{245.69120375, -8.2958899}, cst{245.81068041667, -18.5452347}, cst{247.43823, -18.5272255}, cst{247.45067541667, -19.5271549}, cst{245.82298375, -19.5451660}, cst{245.89144625, -24.8781185}, cst{253.15567041667, -24.7960968}, cst{253.23534875, -30.2123089}, cst{266.00183541667, -30.0606632}, cst{269.50281125, -30.0182076}, cst{269.62546375, -37.0174599}, cst{269.80928375, -45.5163460}, cst{248.57062375, -45.7670517}, cst{248.4947775, -42.2674789}, cst{242.15280625, -42.3366776}, cst{241.94769875, -29.8377628}, cst{236.92998, -29.8896160}, cst{236.81306375, -20.3902016}, cst{240.57177625, -20.3516178}}
cstLists["SCL"] = []cst{cst{346.68096625, -24.8250446}, cst{359.11056458333, -24.8042011}, cst{26.45888875, -24.8729095}, cst{26.350727916667, -39.3726234}, cst{351.69270458333, -39.3127594}, cst{351.6833775, -36.3127670}, cst{346.72751375, -36.3249741}}
cstLists["SCT"] = []cst{cst{275.54952625, -15.9435720}, cst{284.79372, -15.8328123}, cst{284.74405375, -11.8664360}, cst{284.64729291667, -3.8336766}, cst{280.3981875, -3.8842230}, cst{275.3991225, -3.9444826}}
cstLists["SER1"] = []cst{cst{227.85301208333, -0.4742887}, cst{227.78148625, 7.5253930}, cst{227.60549125, 25.5246105}, cst{229.09951625, 25.5380573}, cst{241.80573458333, 25.6641407}, cst{241.85657541667, 21.6644115}, cst{240.1110075, 21.6459675}, cst{240.18107375, 15.6463346}, cst{242.67663, 15.6728001}, cst{242.80966791667, 3.6735139}, cst{245.558595, 3.7033811}, cst{245.6026275, -0.2963768}, cst{245.63838875, -3.5461800}, cst{240.38695375, -3.6025870}, cst{227.88195125, -3.7241600}}
cstLists["SER2"] = []cst{cst{275.35059875, 0.0552235}, cst{275.31423625, 3.0550034}, cst{277.93922375, 3.0867271}, cst{277.92105625, 4.5866175}, cst{275.29601125, 4.5548930}, cst{275.27461041667, 6.3047633}, cst{281.45856875, 6.3791943}, cst{284.525985, 6.4156075}, cst{284.57642541667, 2.1659052}, cst{280.3262325, 2.1153460}, cst{280.35020875, 0.1154895}, cst{280.3981875, -3.8842230}, cst{275.3991225, -3.9444826}, cst{275.54952625, -15.9435720}, cst{265.80019041667, -16.0618820}, cst{259.29742791667, -16.1399899}, cst{259.22206958333, -10.1404381}, cst{265.47349041667, -10.0653696}, cst{265.49440375, -11.7319136}, cst{266.7447, -11.7167768}, cst{266.72375708333, -10.0502338}, cst{271.22371625, -9.9956055}, cst{271.14967375, -3.9960551}, cst{269.14970375, -4.0203514}, cst{269.10103791667, -0.0206471}}
cstLists["SEX"] = []cst{cst{145.34890625, -0.5670585}, cst{145.39841708333, 6.4327669}, cst{162.87601958333, 6.3377299}, cst{162.8497125, -0.6622211}, cst{162.82713875, -6.6621790}, cst{162.80791375, -11.6621428}, cst{145.27027208333, -11.5667810}}
cstLists["TAU"] = []cst{cst{50.836682916667, -1.3029516}, cst{50.85298375, 0.4469725}, cst{50.94641125, 10.3632069}, cst{51.037234583333, 19.4461136}, cst{52.2906225, 19.4343338}, cst{52.426667916667, 31.1003609}, cst{69.4869375, 30.9218750}, cst{69.47678875, 30.2552605}, cst{73.235342916667, 30.2123089}, cst{73.212475416667, 28.7124405}, cst{90.228902916667, 28.5092430}, cst{90.22107125, 28.0092907}, cst{90.1440375, 22.8430862}, cst{87.39379375, 22.8764725}, cst{87.326982083333, 18.0435486}, cst{88.327167083333, 18.0314159}, cst{88.255369583333, 12.5318508}, cst{85.755057083333, 12.5621548}, cst{85.79364625, 15.5619202}, cst{81.7922325, 15.6101446}, cst{81.7987125, 16.1101055}, cst{76.29527875, 16.1754990}, cst{76.28892625, 15.6755352}, cst{71.03402875, 15.7364635}, cst{70.852360416667, 0.2375014}, cst{55.352905416667, 0.4037257}, cst{55.335582083333, -1.3461887}}
cstLists["TEL"] = []cst{cst{307.45880125, -56.5885773}, cst{307.169295, -45.0900002}, cst{289.76964, -45.2775650}, cst{272.3090175, -45.4859734}, cst{272.67225291667, -56.9837723}}
cstLists["TRI"] = []cst{cst{26.744674583333, 25.6263351}, cst{26.76471, 28.6262817}, cst{22.86642, 28.6454391}, cst{22.89742625, 33.6453705}, cst{22.910835, 35.6453362}, cst{31.854250416667, 35.5971375}, cst{31.87109125, 37.3470840}, cst{39.67934125, 37.2931557}, cst{40.43465875, 37.2873878}, cst{40.402382916667, 34.5375137}, cst{42.666467916667, 34.5196762}, cst{42.62838, 31.1865025}, cst{38.10319375, 31.2213154}, cst{38.07014875, 27.8047638}, cst{30.53061625, 27.8550186}, cst{30.51371125, 25.6050701}}
cstLists["TRA"] = []cst{cst{224.16644125, -70.5115433}, cst{224.00363375, -68.0122070}, cst{226.55712625, -67.9909286}, cst{226.35353541667, -64.0751266}, cst{230.16657875, -64.0415649}, cst{230.05456958333, -61.4587479}, cst{232.58976458333, -61.4353065}, cst{232.5498675, -60.4354935}, cst{249.03468125, -60.2644577}, cst{249.08163, -61.2641945}, cst{251.53784708333, -61.2364578}, cst{251.67632125, -63.8189964}, cst{254.1951375, -63.7900925}, cst{254.28351458333, -65.2062531}, cst{255.5423925, -65.1916428}, cst{255.72498291667, -67.6905823}, cst{258.2424825, -67.6610870}, cst{258.47067875, -70.1597443}}
cstLists["TUC"] = []cst{cst{351.99783291667, -74.3124619}, cst{1.5662970833333, -74.3039627}, cst{12.3324375, -74.3185730}, cst{12.295414583333, -75.3185272}, cst{20.654050416667, -75.3472214}, cst{21.20622875, -57.8484154}, cst{351.77839375, -57.8126793}, cst{351.76852208333, -56.3126869}, cst{332.113695, -56.3908348}, cst{332.3985675, -66.8899918}, cst{351.86139125, -66.8125992}}
cstLists["UMA"] = []cst{cst{145.70923791667, 41.4316750}, cst{139.51249041667, 41.4785957}, cst{139.59071125, 46.4782791}, cst{128.44010375, 46.5777283}, cst{128.79913375, 59.5759888}, cst{122.12910125, 59.6433983}, cst{123.08622875, 73.1383743}, cst{140.61547375, 72.9741364}, cst{171.96136958333, 72.8125000}, cst{171.84934625, 65.8126068}, cst{181.57925541667, 65.8039627}, cst{181.58155958333, 63.3039627}, cst{203.55053875, 63.3593445}, cst{203.57364125, 62.3593979}, cst{217.04525375, 62.4414825}, cst{217.25124875, 54.9422379}, cst{211.58439125, 54.9035759}, cst{211.69873208333, 47.9039383}, cst{203.79511375, 47.8599281}, cst{203.74239875, 52.3598061}, cst{182.8185225, 52.3043365}, cst{182.82643375, 44.3043365}, cst{181.59141625, 44.3039627}, cst{181.59450625, 33.3039627}, cst{181.59566375, 28.3039627}, cst{179.60894125, 28.3040466}, cst{166.69399791667, 28.3250256}, cst{166.71422541667, 33.3249931}, cst{163.48940875, 33.3356781}, cst{163.52316875, 39.3356133}, cst{154.3594125, 39.3774109}, cst{154.37822375, 41.3773613}}
cstLists["UMI"] = []cst{cst{195.8206125, 76.3289108}, cst{196.09747375, 69.3293610}, cst{210.65081125, 69.3991165}, cst{210.82055541667, 65.3996506}, cst{235.32956541667, 65.6023483}, cst{235.05063, 69.6009445}, cst{247.8410625, 69.7383041}, cst{247.2207075, 74.7347870}, cst{261.53663708333, 74.9033127}, cst{260.21790458333, 79.8953476}, cst{267.65602041667, 79.9857483}, cst{261.72223041667, 85.9495697}, cst{308.72097, 86.4656219}, cst{308.33135541667, 86.6306305}, cst{343.51066625, 86.8368912}, cst{339.26098791667, 88.6638870}, cst{135.83247125, 87.5689163}, cst{130.40275041667, 86.0975418}, cst{213.0229575, 85.9308090}, cst{216.78285625, 79.4449844}, cst{203.80918958333, 79.3629303}, cst{204.15701875, 76.3638153}}
cstLists["VEL"] = []cst{cst{166.33725625, -57.1744423}, cst{166.45650458333, -40.4246216}, cst{141.73406125, -40.2918739}, cst{141.77159875, -37.2920151}, cst{126.67779875, -37.1600380}, cst{126.57231291667, -43.4095192}, cst{121.03827875, -43.3535042}, cst{120.8616975, -51.1025848}, cst{123.38112875, -51.1285286}, cst{123.32011625, -53.3782196}, cst{127.60929125, -53.4206772}, cst{127.56711875, -54.9204712}, cst{133.38017375, -54.9742203}, cst{133.32365541667, -56.9739723}}
cstLists["VIR"] = []cst{cst{174.35052458333, -0.6916979}, cst{174.36568791667, 10.3082914}, cst{179.60373458333, 10.3040485}, cst{179.60453541667, 13.3040485}, cst{194.0620275, 13.3225126}, cst{194.05906625, 14.3225088}, cst{204.02893041667, 14.3604937}, cst{204.06384875, 7.3605771}, cst{227.78148625, 7.5253930}, cst{227.85301208333, -0.4742887}, cst{221.6030925, -0.5269387}, cst{221.66710791667, -8.5266848}, cst{215.40850625, -8.5731344}, cst{215.51309125, -22.5727749}, cst{194.16687, -22.6773415}, cst{194.1330525, -11.6773882}, cst{179.09676, -11.6957970}, cst{179.09860625, -6.6957974}, cst{174.34229875, -6.6916924}}
cstLists["VOL"] = []cst{cst{98.93724875, -64.1070251}, cst{98.454422916667, -70.1041336}, cst{97.770709583333, -75.1000366}, cst{114.21470375, -75.2899170}, cst{135.24368708333, -75.4954681}, cst{136.09472708333, -64.4990387}, cst{102.70331375, -64.1518784}}
cstLists["VUL"] = []cst{cst{284.33913291667, 21.2478352}, cst{284.27716208333, 25.6641407}, cst{290.16131375, 25.7325745}, cst{290.13264625, 27.7324085}, cst{296.27220125, 27.8011742}, cst{296.25094375, 29.3010578}, cst{315.07258375, 29.4871387}, cst{315.08391458333, 28.4871883}, cst{322.62016375, 28.5480537}, cst{322.66201958333, 24.0482101}, cst{320.15170041667, 24.0289364}, cst{320.18840875, 20.0290813}, cst{317.17878291667, 20.0046406}, cst{309.907665, 19.9399967}, cst{309.89693708333, 20.9399471}, cst{305.13404875, 20.8936996}, cst{305.12540875, 21.6436558}, cst{298.860255, 21.5787334}, cst{298.88568875, 19.4955387}, cst{290.12128791667, 19.3982983}, cst{290.09631125, 21.3148155}}
change := []string{"PSC", "TUC", "PHE", "SCL", "CET", "PEG", "AND", "CAS", "CEP"}
for _, v := range change {
for k, v2 := range cstLists[v] {
if v2.RA < 270 {
cstLists[v][k].RA = v2.RA + 360
}
}
}
}
//选定 RA=277.5 DEC=-40
func isCross(a, b, c, d cst) bool {
var ac, bc, ad, bd, ca, cb, da, db cst
var r1, r2 float64
ac.RA = a.RA - c.RA
ac.DEC = a.DEC - c.DEC
ad.RA = a.RA - d.RA
ad.DEC = a.DEC - d.DEC
r1 = ac.RA*ad.DEC - ad.RA*ac.DEC
bc.RA = b.RA - c.RA
bc.DEC = b.DEC - c.DEC
bd.RA = b.RA - d.RA
bd.DEC = b.DEC - d.DEC
r2 = bc.RA*bd.DEC - bd.RA*bc.DEC
//echo r1.' '.r2;
if r1*r2 > 0 {
return false
}
ca.RA = c.RA - a.RA
ca.DEC = c.DEC - a.DEC
cb.RA = c.RA - b.RA
cb.DEC = c.DEC - b.DEC
r1 = ca.RA*cb.DEC - cb.RA*ca.DEC
da.RA = d.RA - a.RA
da.DEC = d.DEC - a.DEC
db.RA = d.RA - b.RA
db.DEC = d.DEC - b.DEC
r2 = da.RA*db.DEC - db.RA*da.DEC
if r1*r2 > 0 {
return false
}
return true
}
func IsXZ(ra, dec, jde float64) string {
var nra, ndec float64
if cstLists == nil || len(cstLists) == 0 {
initCstData()
}
nra = ra
if ra >= 360 {
nra -= 360
}
nra, ndec = Precess(nra, dec, jde, 2451545.0)
if ra >= 360 && nra < 270 {
nra += 360
}
for k, v := range cstLists {
var count int = 0
for i := 0; i < len(v)-1; i++ {
if k == "UMI" || k == "OCT" {
continue
}
if i == 0 {
if isCross(cst{277.5, -100}, cst{nra, ndec}, v[len(v)-1], v[0]) {
count++
}
}
if isCross(cst{277.5, -100}, cst{nra, ndec}, v[i], v[i+1]) {
count++
}
if FR((nra-277.5)*(v[i].DEC+100)) == FR((v[i].RA-277.5)*(ndec+100)) {
count++
}
}
if count%2 == 1 {
return k
}
}
if nra <= 270 {
ra = ra + 360
return IsXZ(ra, dec, jde)
}
if ndec > 50 {
return "UMI"
} else if ndec < -50 {
return "OCT"
}
return ""
}
func WhichCst(ra, dec, jde float64) string {
cst := make(map[string]string, 88)
cst["AND"] = "仙女座"
cst["ANT"] = "唧筒座"
cst["APS"] = "天燕座"
cst["AQR"] = "宝瓶座"
cst["AQL"] = "天鹰座"
cst["ARA"] = "天坛座"
cst["ARI"] = "白羊座"
cst["AUR"] = "御夫座"
cst["BOO"] = "牧夫座"
cst["CAE"] = "雕具座"
cst["CAM"] = "鹿豹座"
cst["CNC"] = "巨蟹座"
cst["CVN"] = "猎犬座"
cst["CMA"] = "大犬座"
cst["CMI"] = "小犬座"
cst["CAP"] = "摩羯座"
cst["CAR"] = "船底座"
cst["CAS"] = "仙后座"
cst["CEN"] = "半人马座"
cst["CEP"] = "仙王座"
cst["CET"] = "鲸鱼座"
cst["CHA"] = "蝘蜓座"
cst["CIR"] = "圆规座"
cst["COL"] = "天鸽座"
cst["COM"] = "后发座"
cst["CRA"] = "南冕座"
cst["CRB"] = "北冕座"
cst["CRV"] = "乌鸦座"
cst["CRT"] = "巨爵座"
cst["CRU"] = "南十字座"
cst["CYG"] = "天鹅座"
cst["DEL"] = "海豚座"
cst["DOR"] = "剑鱼座"
cst["DRA"] = "天龙座"
cst["EQU"] = "小马座"
cst["ERI"] = "波江座"
cst["FOR"] = "天炉座"
cst["GEM"] = "双子座"
cst["GRU"] = "天鹤座"
cst["HER"] = "武仙座"
cst["HOR"] = "时钟座"
cst["HYA"] = "长蛇座"
cst["HYI"] = "水蛇座"
cst["IND"] = "印第安座"
cst["LAC"] = "蝎虎座"
cst["LEO"] = "狮子座"
cst["LMI"] = "小狮座"
cst["LEP"] = "天兔座"
cst["LIB"] = "天秤座"
cst["LUP"] = "豺狼座"
cst["LYN"] = "天猫座"
cst["LYR"] = "天琴座"
cst["MEN"] = "山案座"
cst["MIC"] = "显微镜座"
cst["MON"] = "麒麟座"
cst["MUS"] = "苍蝇座"
cst["NOR"] = "矩尺座"
cst["OCT"] = "南极座"
cst["OPH"] = "蛇夫座"
cst["ORI"] = "猎户座"
cst["PAV"] = "孔雀座"
cst["PEG"] = "飞马座"
cst["PER"] = "英仙座"
cst["PHE"] = "凤凰座"
cst["PIC"] = "绘架座"
cst["PSC"] = "双鱼座"
cst["PSA"] = "南鱼座"
cst["PUP"] = "船尾座"
cst["PYX"] = "罗盘座"
cst["RET"] = "网罟座"
cst["SGE"] = "天箭座"
cst["SGR"] = "人马座"
cst["SCO"] = "天蝎座"
cst["SCL"] = "玉夫座"
cst["SCT"] = "盾牌座"
cst["SER1"] = "巨蛇座"
cst["SER2"] = "巨蛇座"
cst["SEX"] = "六分仪座"
cst["TAU"] = "金牛座"
cst["TEL"] = "望远镜座"
cst["TRI"] = "三角座"
cst["TRA"] = "南三角座"
cst["TUC"] = "杜鹃座"
cst["UMA"] = "大熊座"
cst["UMI"] = "小熊座"
cst["VEL"] = "船帆座"
cst["VIR"] = "室女座"
cst["VOL"] = "飞鱼座"
cst["VUL"] = "狐狸座"
mystar := IsXZ(ra, dec, jde)
return cst[mystar]
}
+115
View File
@@ -0,0 +1,115 @@
package basic
import "math"
var defDeltaTFn = DefaultDeltaTv2
func DeltaT(date float64, isJDE bool) float64 {
return defDeltaTFn(date, isJDE)
}
func SetDeltaTFn(fn func(float64, bool) float64) {
if fn != nil {
defDeltaTFn = fn
}
}
func GetDeltaTFn() func(float64, bool) float64 {
return defDeltaTFn
}
func DefaultDeltaTv2(date float64, isJd bool) float64 { //传入年或儒略日,传出为秒
if !isJd {
date = JDECalc(int(date), int((date-math.Floor(date))*12)+1, (date-math.Floor(date))*365.25+1)
}
return DeltaTv2(date)
}
// 使用Stephenson等人(2016)和Morrison等人(2021)的拟合和外推公式计算Delta T
// http://astro.ukho.gov.uk/nao/lvm/
// 2010年后的系数已修改以包含2019年后的数据
// 返回Delta T,单位为秒
func DeltaTSplineY(y float64) float64 {
// 积分lod(平均太阳日偏离86400秒的偏差)方程:
// 来自 http://astro.ukho.gov.uk/nao/lvm/
// lod = 1.72 t 3.5 sin(2*pi*(t+0.75)/14) 单位ms/day,其中 t = (y - 1825)/100
// 是从1825年开始的世纪数
// 使用 1ms = 1e-3s 和 1儒略年 = 365.25天,
// lod = 6.2823e-3 * Delta y - 1.278375*sin(2*pi/14*(Delta y /100 + 0.75) 单位s/year
// 其中 Delta y = y - 1825。积分该方程得到
// Integrate[lod, y] = 3.14115e-3*(Delta y)^2 + 894.8625/pi*cos(2*pi/14*(Delta y /100 + 0.75)
// 单位为秒。积分常数设为0。
integratedLod := func(x float64) float64 {
u := x - 1825
return 3.14115e-3*u*u + 284.8435805251424*math.Cos(0.4487989505128276*(0.01*u+0.75))
}
if y < -720 {
// 使用积分lod + 常数
const c = 1.007739546148514
return integratedLod(y) + c
}
if y > 2025 {
// 使用积分lod + 常数
const c = -150.56787057979514
return integratedLod(y) + c
}
// 使用三次样条拟合
y0 := []float64{-720, -100, 400, 1000, 1150, 1300, 1500, 1600, 1650, 1720, 1800, 1810, 1820, 1830, 1840, 1850, 1855, 1860, 1865, 1870, 1875, 1880, 1885, 1890, 1895, 1900, 1905, 1910, 1915, 1920, 1925, 1930, 1935, 1940, 1945, 1950, 1953, 1956, 1959, 1962, 1965, 1968, 1971, 1974, 1977, 1980, 1983, 1986, 1989, 1992, 1995, 1998, 2001, 2004, 2007, 2010, 2013, 2016, 2019, 2022}
y1 := []float64{-100, 400, 1000, 1150, 1300, 1500, 1600, 1650, 1720, 1800, 1810, 1820, 1830, 1840, 1850, 1855, 1860, 1865, 1870, 1875, 1880, 1885, 1890, 1895, 1900, 1905, 1910, 1915, 1920, 1925, 1930, 1935, 1940, 1945, 1950, 1953, 1956, 1959, 1962, 1965, 1968, 1971, 1974, 1977, 1980, 1983, 1986, 1989, 1992, 1995, 1998, 2001, 2004, 2007, 2010, 2013, 2016, 2019, 2022, 2025}
a0 := []float64{20371.848, 11557.668, 6535.116, 1650.393, 1056.647, 681.149, 292.343, 109.127, 43.952, 12.068, 18.367, 15.678, 16.516, 10.804, 7.634, 9.338, 10.357, 9.04, 8.255, 2.371, -1.126, -3.21, -4.388, -3.884, -5.017, -1.977, 4.923, 11.142, 17.479, 21.617, 23.789, 24.418, 24.164, 24.426, 27.05, 28.932, 30.002, 30.76, 32.652, 33.621, 35.093, 37.956, 40.951, 44.244, 47.291, 50.361, 52.936, 54.984, 56.373, 58.453, 60.678, 62.898, 64.083, 64.553, 65.197, 66.061, 66.919, 68.130, 69.250, 69.296}
a1 := []float64{-9999.586, -5822.27, -5671.519, -753.21, -459.628, -421.345, -192.841, -78.697, -68.089, 2.507, -3.481, 0.021, -2.157, -6.018, -0.416, 1.642, -0.486, -0.591, -3.456, -5.593, -2.314, -1.893, 0.101, -0.531, 0.134, 5.715, 6.828, 6.33, 5.518, 3.02, 1.333, 0.052, -0.419, 1.645, 2.499, 1.127, 0.737, 1.409, 1.577, 0.868, 2.275, 3.035, 3.157, 3.199, 3.069, 2.878, 2.354, 1.577, 1.648, 2.235, 2.324, 1.804, 0.674, 0.466, 0.804, 0.839, 1.005, 1.348, 0.594, -0.227}
a2 := []float64{776.247, 1303.151, -298.291, 184.811, 108.771, 61.953, -6.572, 10.505, 38.333, 41.731, -1.126, 4.629, -6.806, 2.944, 2.658, 0.261, -2.389, 2.284, -5.148, 3.011, 0.269, 0.152, 1.842, -2.474, 3.138, 2.443, -1.329, 0.831, -1.643, -0.856, -0.831, -0.449, -0.022, 2.086, -1.232, 0.22, -0.61, 1.282, -1.115, 0.406, 1.002, -0.242, 0.364, -0.323, 0.193, -0.384, -0.14, -0.637, 0.708, -0.121, 0.21, -0.729, -0.402, 0.194, 0.144, -0.109, 0.275, 0.068, -0.822, 0.001}
a3 := []float64{409.16, -503.433, 1085.087, -25.346, -24.641, -29.414, 16.197, 3.018, -2.127, -37.939, 1.918, -3.812, 3.25, -0.096, -0.539, -0.883, 1.558, -2.477, 2.72, -0.914, -0.039, 0.563, -1.438, 1.871, -0.232, -1.257, 0.72, -0.825, 0.262, 0.008, 0.127, 0.142, 0.702, -1.106, 0.614, -0.277, 0.631, -0.799, 0.507, 0.199, -0.414, 0.202, -0.229, 0.172, -0.192, 0.081, -0.165, 0.448, -0.276, 0.11, -0.313, 0.109, 0.199, -0.017, -0.084, 0.128, -0.069, -0.297, 0.274, 0.086}
n := len(y0)
var i int
for i = n - 1; i >= 0; i-- {
if y >= y0[i] {
break
}
}
t := (y - y0[i]) / (y1[i] - y0[i])
dT := a0[i] + t*(a1[i]+t*(a2[i]+t*a3[i]))
return dT
}
func DeltaTv2(jd float64) float64 {
if jd > 2461041.5 || jd < 2441317.5 {
var y float64
if jd >= 2299160.5 {
y = (jd-2451544.5)/365.2425 + 2000
} else {
y = (jd+0.5)/365.25 - 4712
}
return DeltaTSplineY(y)
}
// 闰秒JD值
jdLeaps := []float64{2457754.5, 2457204.5, 2456109.5, 2454832.5,
2453736.5, 2451179.5, 2450630.5, 2450083.5,
2449534.5, 2449169.5, 2448804.5, 2448257.5,
2447892.5, 2447161.5, 2446247.5, 2445516.5,
2445151.5, 2444786.5, 2444239.5, 2443874.5,
2443509.5, 2443144.5, 2442778.5, 2442413.5,
2442048.5, 2441683.5, 2441499.5, 2441133.5}
n := len(jdLeaps)
deltaTSeconds := 42.184
for i := 0; i < n; i++ {
if jd > jdLeaps[i] {
deltaTSeconds += float64(n - i - 1)
break
}
}
return deltaTSeconds
}
func TD2UT(jde float64, utToTD bool) float64 { // true 世界时转力学时CC,false 力学时转世界时VV
deltaTSeconds := DeltaT(jde, true)
if utToTD {
return jde + deltaTSeconds/3600/24
} else {
return jde - deltaTSeconds/3600/24
}
}
+206
View File
@@ -0,0 +1,206 @@
package basic
import "math"
const (
angularDiameterAstronomicalUnitKM = 149597870.7
angularDiameterArcsecPerRadian = 180.0 * 3600.0 / math.Pi
sunEquatorialRadiusKM = 695700.0
moonEquatorialRadiusKM = 1737.4
mercuryEquatorialRadiusKM = 2440.53
venusEquatorialRadiusKM = 6051.8
marsEquatorialRadiusKM = 3396.19
jupiterEquatorialRadiusKM = 71492.0
saturnEquatorialRadiusKM = 60268.0
uranusEquatorialRadiusKM = 25559.0
neptuneEquatorialRadiusKM = 24764.0
)
func angularSemidiameterArcsec(radiusKM, distanceKM float64) float64 {
return math.Asin(radiusKM/distanceKM) * angularDiameterArcsecPerRadian
}
func angularSemidiameterFromAU(radiusKM, distanceAU float64) float64 {
return angularSemidiameterArcsec(radiusKM, distanceAU*angularDiameterAstronomicalUnitKM)
}
// SunSemidiameter 太阳视半径,单位角秒 / apparent solar semidiameter in arcseconds.
func SunSemidiameter(jd float64) float64 {
return SunSemidiameterN(jd, -1)
}
// SunSemidiameterN 太阳视半径(截断版),单位角秒 / truncated apparent solar semidiameter in arcseconds.
func SunSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(sunEquatorialRadiusKM, EarthAwayN(jd, n))
}
// SunDiameter 太阳视直径,单位角秒 / apparent solar diameter in arcseconds.
func SunDiameter(jd float64) float64 {
return SunDiameterN(jd, -1)
}
// SunDiameterN 太阳视直径(截断版),单位角秒 / truncated apparent solar diameter in arcseconds.
func SunDiameterN(jd float64, n int) float64 {
return 2 * SunSemidiameterN(jd, n)
}
// MoonSemidiameter 月亮视半径,单位角秒 / apparent lunar semidiameter in arcseconds.
func MoonSemidiameter(jd float64) float64 {
return MoonSemidiameterN(jd, -1)
}
// MoonSemidiameterN 月亮视半径(截断版),单位角秒 / truncated apparent lunar semidiameter in arcseconds.
func MoonSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterArcsec(moonEquatorialRadiusKM, HMoonAwayN(jd, n))
}
// MoonDiameter 月亮视直径,单位角秒 / apparent lunar diameter in arcseconds.
func MoonDiameter(jd float64) float64 {
return MoonDiameterN(jd, -1)
}
// MoonDiameterN 月亮视直径(截断版),单位角秒 / truncated apparent lunar diameter in arcseconds.
func MoonDiameterN(jd float64, n int) float64 {
return 2 * MoonSemidiameterN(jd, n)
}
// MercurySemidiameter 水星视半径,单位角秒 / apparent Mercury semidiameter in arcseconds.
func MercurySemidiameter(jd float64) float64 {
return MercurySemidiameterN(jd, -1)
}
// MercurySemidiameterN 水星视半径(截断版),单位角秒 / truncated apparent Mercury semidiameter in arcseconds.
func MercurySemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(mercuryEquatorialRadiusKM, EarthMercuryAwayN(jd, n))
}
// MercuryDiameter 水星视直径,单位角秒 / apparent Mercury diameter in arcseconds.
func MercuryDiameter(jd float64) float64 {
return MercuryDiameterN(jd, -1)
}
// MercuryDiameterN 水星视直径(截断版),单位角秒 / truncated apparent Mercury diameter in arcseconds.
func MercuryDiameterN(jd float64, n int) float64 {
return 2 * MercurySemidiameterN(jd, n)
}
// VenusSemidiameter 金星视半径,单位角秒 / apparent Venus semidiameter in arcseconds.
func VenusSemidiameter(jd float64) float64 {
return VenusSemidiameterN(jd, -1)
}
// VenusSemidiameterN 金星视半径(截断版),单位角秒 / truncated apparent Venus semidiameter in arcseconds.
func VenusSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(venusEquatorialRadiusKM, EarthVenusAwayN(jd, n))
}
// VenusDiameter 金星视直径,单位角秒 / apparent Venus diameter in arcseconds.
func VenusDiameter(jd float64) float64 {
return VenusDiameterN(jd, -1)
}
// VenusDiameterN 金星视直径(截断版),单位角秒 / truncated apparent Venus diameter in arcseconds.
func VenusDiameterN(jd float64, n int) float64 {
return 2 * VenusSemidiameterN(jd, n)
}
// MarsSemidiameter 火星视半径,单位角秒 / apparent Mars semidiameter in arcseconds.
func MarsSemidiameter(jd float64) float64 {
return MarsSemidiameterN(jd, -1)
}
// MarsSemidiameterN 火星视半径(截断版),单位角秒 / truncated apparent Mars semidiameter in arcseconds.
func MarsSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(marsEquatorialRadiusKM, EarthMarsAwayN(jd, n))
}
// MarsDiameter 火星视直径,单位角秒 / apparent Mars diameter in arcseconds.
func MarsDiameter(jd float64) float64 {
return MarsDiameterN(jd, -1)
}
// MarsDiameterN 火星视直径(截断版),单位角秒 / truncated apparent Mars diameter in arcseconds.
func MarsDiameterN(jd float64, n int) float64 {
return 2 * MarsSemidiameterN(jd, n)
}
// JupiterSemidiameter 木星视半径,单位角秒 / apparent Jupiter semidiameter in arcseconds.
func JupiterSemidiameter(jd float64) float64 {
return JupiterSemidiameterN(jd, -1)
}
// JupiterSemidiameterN 木星视半径(截断版),单位角秒 / truncated apparent Jupiter semidiameter in arcseconds.
func JupiterSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(jupiterEquatorialRadiusKM, EarthJupiterAwayN(jd, n))
}
// JupiterDiameter 木星视直径,单位角秒 / apparent Jupiter diameter in arcseconds.
func JupiterDiameter(jd float64) float64 {
return JupiterDiameterN(jd, -1)
}
// JupiterDiameterN 木星视直径(截断版),单位角秒 / truncated apparent Jupiter diameter in arcseconds.
func JupiterDiameterN(jd float64, n int) float64 {
return 2 * JupiterSemidiameterN(jd, n)
}
// SaturnSemidiameter 土星视半径,单位角秒 / apparent Saturn semidiameter in arcseconds.
func SaturnSemidiameter(jd float64) float64 {
return SaturnSemidiameterN(jd, -1)
}
// SaturnSemidiameterN 土星视半径(截断版),单位角秒 / truncated apparent Saturn semidiameter in arcseconds.
func SaturnSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(saturnEquatorialRadiusKM, EarthSaturnAwayN(jd, n))
}
// SaturnDiameter 土星视直径,单位角秒 / apparent Saturn diameter in arcseconds.
func SaturnDiameter(jd float64) float64 {
return SaturnDiameterN(jd, -1)
}
// SaturnDiameterN 土星视直径(截断版),单位角秒 / truncated apparent Saturn diameter in arcseconds.
func SaturnDiameterN(jd float64, n int) float64 {
return 2 * SaturnSemidiameterN(jd, n)
}
// UranusSemidiameter 天王星视半径,单位角秒 / apparent Uranus semidiameter in arcseconds.
func UranusSemidiameter(jd float64) float64 {
return UranusSemidiameterN(jd, -1)
}
// UranusSemidiameterN 天王星视半径(截断版),单位角秒 / truncated apparent Uranus semidiameter in arcseconds.
func UranusSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(uranusEquatorialRadiusKM, EarthUranusAwayN(jd, n))
}
// UranusDiameter 天王星视直径,单位角秒 / apparent Uranus diameter in arcseconds.
func UranusDiameter(jd float64) float64 {
return UranusDiameterN(jd, -1)
}
// UranusDiameterN 天王星视直径(截断版),单位角秒 / truncated apparent Uranus diameter in arcseconds.
func UranusDiameterN(jd float64, n int) float64 {
return 2 * UranusSemidiameterN(jd, n)
}
// NeptuneSemidiameter 海王星视半径,单位角秒 / apparent Neptune semidiameter in arcseconds.
func NeptuneSemidiameter(jd float64) float64 {
return NeptuneSemidiameterN(jd, -1)
}
// NeptuneSemidiameterN 海王星视半径(截断版),单位角秒 / truncated apparent Neptune semidiameter in arcseconds.
func NeptuneSemidiameterN(jd float64, n int) float64 {
return angularSemidiameterFromAU(neptuneEquatorialRadiusKM, EarthNeptuneAwayN(jd, n))
}
// NeptuneDiameter 海王星视直径,单位角秒 / apparent Neptune diameter in arcseconds.
func NeptuneDiameter(jd float64) float64 {
return NeptuneDiameterN(jd, -1)
}
// NeptuneDiameterN 海王星视直径(截断版),单位角秒 / truncated apparent Neptune diameter in arcseconds.
func NeptuneDiameterN(jd float64, n int) float64 {
return 2 * NeptuneSemidiameterN(jd, n)
}
+102
View File
@@ -0,0 +1,102 @@
package basic
import (
"encoding/json"
"math"
"os"
"testing"
"time"
)
type angularDiameterSample struct {
InputUTC string `json:"input_utc"`
Values map[string]float64 `json:"values"`
}
func TestAngularDiametersMatchHorizonsBaseline(t *testing.T) {
// Baseline is generated from JPL Horizons by scripts/generate_angular_diameter_baseline.sh.
data, err := os.ReadFile("testdata/angular_diameter_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []angularDiameterSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
type diameterCase struct {
name string
diameter func(float64) float64
semidiameter func(float64) float64
baselineKey string
toleranceArcsec float64
}
cases := []diameterCase{
{name: "Sun", diameter: SunDiameter, semidiameter: SunSemidiameter, baselineKey: "sun", toleranceArcsec: 0.01},
{name: "Moon", diameter: MoonDiameter, semidiameter: MoonSemidiameter, baselineKey: "moon", toleranceArcsec: 0.2},
{name: "Mercury", diameter: MercuryDiameter, semidiameter: MercurySemidiameter, baselineKey: "mercury", toleranceArcsec: 0.01},
{name: "Venus", diameter: VenusDiameter, semidiameter: VenusSemidiameter, baselineKey: "venus", toleranceArcsec: 0.01},
{name: "Mars", diameter: MarsDiameter, semidiameter: MarsSemidiameter, baselineKey: "mars", toleranceArcsec: 0.01},
{name: "Jupiter", diameter: JupiterDiameter, semidiameter: JupiterSemidiameter, baselineKey: "jupiter", toleranceArcsec: 0.01},
{name: "Saturn", diameter: SaturnDiameter, semidiameter: SaturnSemidiameter, baselineKey: "saturn", toleranceArcsec: 0.01},
{name: "Uranus", diameter: UranusDiameter, semidiameter: UranusSemidiameter, baselineKey: "uranus", toleranceArcsec: 0.01},
{name: "Neptune", diameter: NeptuneDiameter, semidiameter: NeptuneSemidiameter, baselineKey: "neptune", toleranceArcsec: 0.01},
}
maxDiffs := make(map[string]float64, len(cases))
for _, sample := range samples {
date, err := time.Parse(time.RFC3339, sample.InputUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.InputUTC, err)
}
jd := TD2UT(Date2JDE(date.UTC()), true)
for _, tc := range cases {
want := sample.Values[tc.baselineKey]
got := tc.diameter(jd)
diff := math.Abs(got - want)
if diff > maxDiffs[tc.name] {
maxDiffs[tc.name] = diff
}
if math.Abs(got-want) > tc.toleranceArcsec {
t.Fatalf("%s diameter mismatch at %s: got %.9f want %.9f tolerance %.9f", tc.name, sample.InputUTC, got, want, tc.toleranceArcsec)
}
semi := tc.semidiameter(jd)
if math.Abs(got-2*semi) > 1e-12 {
t.Fatalf("%s diameter/semidiameter mismatch at %s: diameter %.12f semidiameter %.12f", tc.name, sample.InputUTC, got, semi)
}
}
}
for _, tc := range cases {
t.Logf("%s diameter max diff: %.6f arcsec", tc.name, maxDiffs[tc.name])
}
}
func TestAngularDiameterNFullMatchesDefault(t *testing.T) {
jd := TD2UT(Date2JDE(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)), true)
cases := []struct {
name string
diameter func(float64) float64
diameterN func(float64, int) float64
semidiameter func(float64) float64
semidiameterN func(float64, int) float64
}{
{"Sun", SunDiameter, SunDiameterN, SunSemidiameter, SunSemidiameterN},
{"Moon", MoonDiameter, MoonDiameterN, MoonSemidiameter, MoonSemidiameterN},
{"Mercury", MercuryDiameter, MercuryDiameterN, MercurySemidiameter, MercurySemidiameterN},
{"Venus", VenusDiameter, VenusDiameterN, VenusSemidiameter, VenusSemidiameterN},
{"Mars", MarsDiameter, MarsDiameterN, MarsSemidiameter, MarsSemidiameterN},
{"Jupiter", JupiterDiameter, JupiterDiameterN, JupiterSemidiameter, JupiterSemidiameterN},
{"Saturn", SaturnDiameter, SaturnDiameterN, SaturnSemidiameter, SaturnSemidiameterN},
{"Uranus", UranusDiameter, UranusDiameterN, UranusSemidiameter, UranusSemidiameterN},
{"Neptune", NeptuneDiameter, NeptuneDiameterN, NeptuneSemidiameter, NeptuneSemidiameterN},
}
for _, tc := range cases {
assertSameFloat(t, tc.name+".Diameter", tc.diameter(jd), tc.diameterN(jd, -1))
assertSameFloat(t, tc.name+".Semidiameter", tc.semidiameter(jd), tc.semidiameterN(jd, -1))
}
}
+7 -6
View File
@@ -1,8 +1,9 @@
package basic
import (
. "b612.me/astro/tools"
"math"
. "b612.me/astro/tools"
)
//地球常数
@@ -20,19 +21,19 @@ func HeightDistance(height float64) float64 {
// HeightDistance 高度(单位:米)与地平线下角度的关系(单位:度)
func HeightDegree(height float64) float64 {
return math.Acos((EARTH_AVERAGE_RADIUS)/(EARTH_AVERAGE_RADIUS+height)) * 180 / math.Pi / 2
return math.Acos((EARTH_AVERAGE_RADIUS)/(EARTH_AVERAGE_RADIUS+height)) * 180 / math.Pi
}
// HeightDistanceByLat 不同纬度下高度与地平线距离的关系(单位:米)
func HeightDistanceByLat(height, lat float64) float64 {
raduis := GeocentricRadius(lat)
return math.Acos((raduis)/(raduis+height)) * raduis
radius := GeocentricRadius(lat)
return math.Acos((radius)/(radius+height)) * radius
}
// HeightDegreeByLat 不同纬度下高度(单位:米)与地平线下角度的关系(单位:度)
func HeightDegreeByLat(height, lat float64) float64 {
raduis := GeocentricRadius(lat)
return math.Acos((raduis)/(raduis+height)) * 180 / math.Pi / 2
radius := GeocentricRadius(lat)
return math.Acos((radius)/(radius+height)) * 180 / math.Pi
}
// GeocentricRadius 地心直径与纬度的关系
+26 -5
View File
@@ -1,13 +1,34 @@
package basic
import (
"fmt"
"math"
"testing"
)
func Test_EarthFn(t *testing.T) {
fmt.Println(HeightDistance(10000))
//近似算法,差距在接受范围内?
fmt.Println(math.Sqrt(((EARTH_AVERAGE_RADIUS)*2 + 10000) * 10000))
func TestHeightDegreeMatchesSphericalArc(t *testing.T) {
height := 10000.0
got := HeightDegree(height)
want := HeightDistance(height) / EARTH_AVERAGE_RADIUS * 180 / math.Pi
if math.Abs(got-want) > 1e-12 {
t.Fatalf("HeightDegree mismatch: got %.15f want %.15f", got, want)
}
}
func TestHeightDegreeByLatMatchesSphericalArc(t *testing.T) {
height := 10000.0
lat := 45.0
radius := GeocentricRadius(lat)
got := HeightDegreeByLat(height, lat)
want := HeightDistanceByLat(height, lat) / radius * 180 / math.Pi
if math.Abs(got-want) > 1e-12 {
t.Fatalf("HeightDegreeByLat mismatch: got %.15f want %.15f", got, want)
}
}
func TestHeightDegreeReferenceValue(t *testing.T) {
got := HeightDegree(10000)
want := 3.20801665537668
if math.Abs(got-want) > 1e-12 {
t.Fatalf("HeightDegree(10000) = %.15f want %.15f", got, want)
}
}
+151
View File
@@ -0,0 +1,151 @@
package basic
import (
"math"
"testing"
"time"
)
func TestLunarEclipseDiagramIncludesContacts(t *testing.T) {
diagram := LunarEclipseDiagram(JDECalc(2026, 3, 3), LunarEclipseDiagramOptions{StepDays: 10.0 / 1440.0})
if diagram.Eclipse.Type != LunarEclipseTotal {
t.Fatalf("unexpected eclipse type: got %s want %s", diagram.Eclipse.Type, LunarEclipseTotal)
}
if diagram.MoonRadius != 1 {
t.Fatalf("moon radius mismatch: got %.9f want 1", diagram.MoonRadius)
}
if !(diagram.PenumbraRadius > diagram.UmbraRadius && diagram.UmbraRadius > diagram.MoonRadius) {
t.Fatalf(
"unexpected radii: penumbra=%.9f umbra=%.9f moon=%.9f",
diagram.PenumbraRadius,
diagram.UmbraRadius,
diagram.MoonRadius,
)
}
labels := map[string]bool{}
for _, point := range diagram.Points {
if point.Label != "" {
labels[point.Label] = true
}
}
for _, label := range []string{"P1", "U1", "U2", "Greatest", "U3", "U4", "P4"} {
if !labels[label] {
t.Fatalf("missing label %s in diagram points", label)
}
}
}
func TestLocalSolarEclipseDiagramIncludesContacts(t *testing.T) {
diagram := LocalSolarEclipseDiagram(
TD2UT(Date2JDE(time.Date(2024, 4, 8, 12, 0, 0, 0, time.UTC)), true),
-96.7970,
32.7767,
0,
LocalSolarEclipseDiagramOptions{StepDays: 5.0 / 1440.0},
)
if diagram.Eclipse.Type == SolarEclipseNone {
t.Fatalf("expected local solar eclipse")
}
if len(diagram.Frames) == 0 {
t.Fatalf("expected diagram frames")
}
labels := map[string]bool{}
for _, frame := range diagram.Frames {
if frame.SunRadius != 1 {
t.Fatalf("sun radius mismatch: got %.9f want 1", frame.SunRadius)
}
if !(frame.MoonRadius > 0) {
t.Fatalf("invalid moon radius: %.9f", frame.MoonRadius)
}
if math.IsNaN(frame.MoonX) || math.IsNaN(frame.MoonY) {
t.Fatalf("invalid moon position: x=%f y=%f", frame.MoonX, frame.MoonY)
}
if frame.Label != "" {
labels[frame.Label] = true
}
}
for _, label := range []string{"C1", "Greatest", "C4"} {
if !labels[label] {
t.Fatalf("missing label %s in diagram frames", label)
}
}
}
func TestLocalSolarEclipseDiagramTimesKeepCoincidentLabels(t *testing.T) {
eclipse := LocalSolarEclipseResult{
HasPartial: true,
HasCentral: true,
PartialStart: 1,
GreatestEclipse: 2,
CentralStart: 2,
CentralEnd: 2,
PartialEnd: 3,
}
times, _ := localSolarEclipseDiagramTimes(eclipse, 0.5)
var coincident localSolarEclipseDiagramTime
found := false
for _, item := range times {
if item.jde == 2 {
coincident = item
found = true
break
}
}
if !found {
t.Fatalf("missing coincident event time")
}
want := []string{"C2", "Greatest", "C3"}
if len(coincident.labels) != len(want) {
t.Fatalf("coincident labels = %v, want %v", coincident.labels, want)
}
for i, label := range want {
if coincident.labels[i] != label {
t.Fatalf("coincident labels = %v, want %v", coincident.labels, want)
}
}
if got := localSolarEclipseDiagramPrimaryLabel(coincident.labels); got != "Greatest" {
t.Fatalf("primary label = %q, want %q", got, "Greatest")
}
}
func TestLunarEclipseDiagramTimesKeepCoincidentLabels(t *testing.T) {
eclipse := LunarEclipseResult{
HasPenumbral: true,
HasPartial: true,
HasTotal: true,
PenumbralStart: 1,
PartialStart: 1.5,
TotalStart: 2,
Maximum: 2,
TotalEnd: 2,
PartialEnd: 2.5,
PenumbralEnd: 3,
}
times, _ := lunarEclipseDiagramTimes(eclipse, 0.5)
var coincident lunarEclipseDiagramTime
found := false
for _, item := range times {
if item.jde == 2 {
coincident = item
found = true
break
}
}
if !found {
t.Fatalf("missing coincident event time")
}
want := []string{"U2", "Greatest", "U3"}
if len(coincident.labels) != len(want) {
t.Fatalf("coincident labels = %v, want %v", coincident.labels, want)
}
for i, label := range want {
if coincident.labels[i] != label {
t.Fatalf("coincident labels = %v, want %v", coincident.labels, want)
}
}
if got := lunarEclipseDiagramPrimaryLabel(coincident.labels); got != "Greatest" {
t.Fatalf("primary label = %q, want %q", got, "Greatest")
}
}
+76
View File
@@ -0,0 +1,76 @@
package basic
import "math"
const exactEventTolerance = 2.0 / 86400.0
func sameEventJD(a, b float64) bool {
return math.Abs(a-b) <= exactEventTolerance
}
func sameEventUTQueryTT(eventUT, queryTT float64) bool {
return math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) <= exactEventTolerance
}
func closestEventUTToQueryTT(queryTT, best float64, candidates ...float64) float64 {
bestAbs := math.Abs(eventUTQueryTTDelta(best, queryTT))
for _, candidate := range candidates {
candidateAbs := math.Abs(eventUTQueryTTDelta(candidate, queryTT))
if candidateAbs < bestAbs {
best = candidate
bestAbs = candidateAbs
}
}
return best
}
type phaseEventSearchFunc func(jde, degree float64, next uint8) float64
type simpleEventSearchFunc func(jde float64) float64
func inclusiveLastPhaseEvent(jde, degree float64, fn phaseEventSearchFunc) float64 {
last := fn(jde, degree, 0)
next := fn(jde, degree, 1)
if eventUTQueryBeforeOrEqual(next, jde) && eventUTQueryAfterOrEqual(next, jde) {
return next
}
if eventUTQueryBeforeOrEqual(last, jde) {
return last
}
return last
}
func inclusiveNextPhaseEvent(jde, degree float64, fn phaseEventSearchFunc) float64 {
last := fn(jde, degree, 0)
if eventUTQueryBeforeOrEqual(last, jde) && eventUTQueryAfterOrEqual(last, jde) {
return last
}
next := fn(jde, degree, 1)
if eventUTQueryAfterOrEqual(next, jde) {
return next
}
return next
}
func inclusiveLastSimpleEvent(jde float64, lastFn, nextFn simpleEventSearchFunc) float64 {
last := lastFn(jde)
next := nextFn(jde)
if eventUTQueryBeforeOrEqual(next, jde) && eventUTQueryAfterOrEqual(next, jde) {
return next
}
if eventUTQueryBeforeOrEqual(last, jde) {
return last
}
return last
}
func inclusiveNextSimpleEvent(jde float64, lastFn, nextFn simpleEventSearchFunc) float64 {
last := lastFn(jde)
if eventUTQueryBeforeOrEqual(last, jde) && eventUTQueryAfterOrEqual(last, jde) {
return last
}
next := nextFn(jde)
if eventUTQueryAfterOrEqual(next, jde) {
return next
}
return next
}
+97
View File
@@ -0,0 +1,97 @@
package basic
import "math"
func eventFixedScanRefine(seed, halfWindow, step float64, fn func(float64) float64) float64 {
start := seed - halfWindow
bestJD := start
bestAbs := math.Abs(fn(start))
samples := int(math.Round((2 * halfWindow) / step))
for i := 1; i < samples; i++ {
candidateJD := start + float64(i)*step
candidateAbs := math.Abs(fn(candidateJD))
if candidateAbs < bestAbs {
bestAbs = candidateAbs
bestJD = candidateJD
}
}
return bestJD
}
func eventZeroBracket(leftJD, leftVal, centerJD, centerVal, rightJD, rightVal float64) (float64, float64, float64, float64, bool) {
if leftVal == 0 {
return leftJD, leftJD, leftVal, leftVal, true
}
if centerVal == 0 {
return centerJD, centerJD, centerVal, centerVal, true
}
if rightVal == 0 {
return rightJD, rightJD, rightVal, rightVal, true
}
if leftVal*centerVal < 0 {
return leftJD, centerJD, leftVal, centerVal, true
}
if centerVal*rightVal < 0 {
return centerJD, rightJD, centerVal, rightVal, true
}
if leftVal*rightVal < 0 {
return leftJD, rightJD, leftVal, rightVal, true
}
return 0, 0, 0, 0, false
}
// eventZeroRefine 细化 seed 附近的零点;无可用括号区间时退回固定步长扫描。
func eventZeroRefine(seed, halfWindow, step float64, fn func(float64) float64) float64 {
leftJD := seed - halfWindow
centerJD := seed
rightJD := seed + halfWindow
leftVal := fn(leftJD)
centerVal := fn(centerJD)
rightVal := fn(rightJD)
bestJD := centerJD
bestAbs := math.Abs(centerVal)
if candidateAbs := math.Abs(leftVal); candidateAbs < bestAbs {
bestAbs = candidateAbs
bestJD = leftJD
}
if candidateAbs := math.Abs(rightVal); candidateAbs < bestAbs {
bestAbs = candidateAbs
bestJD = rightJD
}
bracketLeftJD, bracketRightJD, bracketLeftVal, bracketRightVal, ok := eventZeroBracket(leftJD, leftVal, centerJD, centerVal, rightJD, rightVal)
if !ok {
return eventFixedScanRefine(seed, halfWindow, step, fn)
}
if bracketLeftJD == bracketRightJD {
return bracketLeftJD
}
for i := 0; i < 8; i++ {
candidateJD := (bracketLeftJD + bracketRightJD) / 2
if bracketRightVal != bracketLeftVal {
secantJD := bracketRightJD - bracketRightVal*(bracketRightJD-bracketLeftJD)/(bracketRightVal-bracketLeftVal)
if secantJD > bracketLeftJD && secantJD < bracketRightJD {
candidateJD = secantJD
}
}
candidateVal := fn(candidateJD)
candidateAbs := math.Abs(candidateVal)
if candidateAbs < bestAbs {
bestAbs = candidateAbs
bestJD = candidateJD
}
if candidateVal == 0 || math.Abs(bracketRightJD-bracketLeftJD) <= step {
break
}
if bracketLeftVal*candidateVal < 0 {
bracketRightJD = candidateJD
bracketRightVal = candidateVal
continue
}
bracketLeftJD = candidateJD
bracketLeftVal = candidateVal
}
return bestJD
}
+25
View File
@@ -0,0 +1,25 @@
package basic
import (
"math"
"testing"
)
func TestEventZeroRefineFindsNearbyRoot(t *testing.T) {
root := 0.123456789
got := eventZeroRefine(root+0.00002, 0.0003, 1e-7, func(x float64) float64 {
return x - root
})
if math.Abs(got-root) > 1e-12 {
t.Fatalf("got %.15f want %.15f", got, root)
}
}
func TestEventZeroRefineFallsBackToFixedScan(t *testing.T) {
got := eventZeroRefine(0.1, 1, 0.1, func(x float64) float64 {
return x*x + 1
})
if math.Abs(got) > 1e-12 {
t.Fatalf("got %.15f want 0", got)
}
}
+183
View File
@@ -0,0 +1,183 @@
package basic
import "math"
const innerEventEpsilon = 4.0 / 86400.0
func eventQueryTTAsUT(queryTT float64) float64 {
return TD2UT(queryTT, false)
}
func eventUTQueryTTDelta(eventUT, queryTT float64) float64 {
return eventUT - eventQueryTTAsUT(queryTT)
}
func eventUTQueryBeforeOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) <= innerEventEpsilon
}
func eventUTQueryAfterOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) >= -innerEventEpsilon
}
func eventUTNextQueryTT(eventUT float64) float64 {
return TD2UT(eventUT, true) + 1.0
}
func eventUTLastQueryTT(eventUT float64) float64 {
return TD2UT(eventUT, true) - 1.0
}
func innerNextCycleOffset(delta, period float64) float64 {
if delta <= 0 {
return -delta * period / 360.0
}
return (360.0 - delta) * period / 360.0
}
func innerLastCycleOffset(delta, period float64) float64 {
if delta >= 0 {
return delta * period / 360.0
}
return (360.0 + delta) * period / 360.0
}
func clampFloat64(v, min, max float64) float64 {
if v < min {
return min
}
if v > max {
return max
}
return v
}
func scanWindowForMinAbs(start, end, step float64, fn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if step <= 0 || end == start {
return start
}
bestJD := start
bestAbs := math.Abs(fn(start))
for jd := start + step; jd < end; jd += step {
candidateAbs := math.Abs(fn(jd))
if candidateAbs < bestAbs {
bestAbs = candidateAbs
bestJD = jd
}
}
endAbs := math.Abs(fn(end))
if endAbs < bestAbs {
return end
}
return bestJD
}
func scanWindowForMax(start, end, step float64, fn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if step <= 0 || end == start {
return start
}
bestJD := start
bestVal := fn(start)
for jd := start + step; jd < end; jd += step {
candidateVal := fn(jd)
if candidateVal > bestVal {
bestVal = candidateVal
bestJD = jd
}
}
endVal := fn(end)
if endVal > bestVal {
return end
}
return bestJD
}
func boundedEventZeroRefine(seed, start, end, halfWindow, step float64, fn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if end <= start {
return start
}
maxHalfWindow := (end - start) / 2
if halfWindow > maxHalfWindow {
halfWindow = maxHalfWindow
}
if halfWindow <= 0 {
return clampFloat64(seed, start, end)
}
seed = clampFloat64(seed, start+halfWindow, end-halfWindow)
return eventZeroRefine(seed, halfWindow, step, fn)
}
func zeroEventInWindow(start, end, coarseStep, halfWindow, refineStep float64, coarseFn, exactFn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if end <= start {
return start
}
rangeDays := end - start
if coarseStep <= 0 || coarseStep > rangeDays {
coarseStep = rangeDays / 6.0
}
if coarseStep < 0.5 {
coarseStep = 0.5
}
if refineStep <= 0 {
refineStep = 0.5 / 86400.0
}
if halfWindow <= 0 {
halfWindow = coarseStep
}
guess := scanWindowForMinAbs(start, end, coarseStep, coarseFn)
return boundedEventZeroRefine(guess, start, end, halfWindow, refineStep, exactFn)
}
func maximizeInWindow(start, end, coarseStep float64, coarseFn, exactFn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if end <= start {
return start
}
rangeDays := end - start
if coarseStep <= 0 || coarseStep > rangeDays {
coarseStep = rangeDays / 6.0
}
if coarseStep < 0.5 {
coarseStep = 0.5
}
guess := scanWindowForMax(start, end, coarseStep, coarseFn)
left := clampFloat64(guess-coarseStep, start, end)
right := clampFloat64(guess+coarseStep, start, end)
if right-left <= innerEventEpsilon {
return guess
}
for i := 0; i < 20; i++ {
third := (right - left) / 3.0
leftThird := left + third
rightThird := right - third
if exactFn(leftThird) <= exactFn(rightThird) {
left = leftThird
continue
}
right = rightThird
}
bestJD := guess
bestVal := exactFn(bestJD)
for _, jd := range []float64{left, (left + right) / 2.0, right} {
candidateVal := exactFn(jd)
if candidateVal > bestVal {
bestVal = candidateVal
bestJD = jd
}
}
return bestJD
}
+45
View File
@@ -0,0 +1,45 @@
package basic
import "testing"
func TestInnerPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
cases := []struct {
name string
seed float64
lastFn func(float64) float64
nextFn func(float64) float64
}{
{name: "MercuryConjunction", seed: NextMercuryConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryConjunction, nextFn: NextMercuryConjunction},
{name: "MercuryInferior", seed: NextMercuryInferiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryInferiorConjunctionInclusive, nextFn: NextMercuryInferiorConjunctionInclusive},
{name: "MercurySuperior", seed: NextMercurySuperiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercurySuperiorConjunctionInclusive, nextFn: NextMercurySuperiorConjunctionInclusive},
{name: "MercuryRetrograde", seed: NextMercuryRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryRetrogradeInclusive, nextFn: NextMercuryRetrogradeInclusive},
{name: "MercuryP2R", seed: NextMercuryProgradeToRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryProgradeToRetrogradeInclusive, nextFn: NextMercuryProgradeToRetrogradeInclusive},
{name: "MercuryR2P", seed: NextMercuryRetrogradeToProgradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryRetrogradeToProgradeInclusive, nextFn: NextMercuryRetrogradeToProgradeInclusive},
{name: "MercuryGreatestElongation", seed: NextMercuryGreatestElongationInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryGreatestElongationInclusive, nextFn: NextMercuryGreatestElongationInclusive},
{name: "MercuryEastElongation", seed: NextMercuryGreatestElongationEastInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryGreatestElongationEastInclusive, nextFn: NextMercuryGreatestElongationEastInclusive},
{name: "MercuryWestElongation", seed: NextMercuryGreatestElongationWestInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryGreatestElongationWestInclusive, nextFn: NextMercuryGreatestElongationWestInclusive},
{name: "VenusConjunction", seed: NextVenusConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusConjunction, nextFn: NextVenusConjunction},
{name: "VenusInferior", seed: NextVenusInferiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusInferiorConjunctionInclusive, nextFn: NextVenusInferiorConjunctionInclusive},
{name: "VenusSuperior", seed: NextVenusSuperiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusSuperiorConjunctionInclusive, nextFn: NextVenusSuperiorConjunctionInclusive},
{name: "VenusRetrograde", seed: NextVenusRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusRetrogradeInclusive, nextFn: NextVenusRetrogradeInclusive},
{name: "VenusP2R", seed: NextVenusProgradeToRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusProgradeToRetrogradeInclusive, nextFn: NextVenusProgradeToRetrogradeInclusive},
{name: "VenusR2P", seed: NextVenusRetrogradeToProgradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusRetrogradeToProgradeInclusive, nextFn: NextVenusRetrogradeToProgradeInclusive},
{name: "VenusGreatestElongation", seed: NextVenusGreatestElongationInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusGreatestElongationInclusive, nextFn: NextVenusGreatestElongationInclusive},
{name: "VenusEastElongation", seed: NextVenusGreatestElongationEastInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusGreatestElongationEastInclusive, nextFn: NextVenusGreatestElongationEastInclusive},
{name: "VenusWestElongation", seed: NextVenusGreatestElongationWestInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusGreatestElongationWestInclusive, nextFn: NextVenusGreatestElongationWestInclusive},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
queryTT := TD2UT(tc.seed, true)
last := tc.lastFn(queryTT)
next := tc.nextFn(queryTT)
if !sameEventJD(last, tc.seed) {
t.Fatalf("last exact boundary mismatch: got %.12f want %.12f", last, tc.seed)
}
if !sameEventJD(next, tc.seed) {
t.Fatalf("next exact boundary mismatch: got %.12f want %.12f", next, tc.seed)
}
})
}
}
+165
View File
@@ -0,0 +1,165 @@
package basic
import (
"encoding/json"
"os"
"strings"
"testing"
"time"
)
type innerBaselineFile struct {
Events []innerBaselineEvent `json:"events"`
}
type innerBaselineEvent struct {
Planet string `json:"planet"`
Kind string `json:"kind"`
NAOJHintJST string `json:"naoj_hint_jst"`
Precision string `json:"precision"`
CandidateJST string `json:"candidate_jst"`
VerifiedJST string `json:"verified_jst"`
CandidateSource string `json:"candidate_source"`
}
func loadInnerBaseline(t *testing.T) innerBaselineFile {
t.Helper()
paths := [][]string{
{
"testdata/jpl_inner_event_baseline.json",
"basic/testdata/jpl_inner_event_baseline.json",
},
{
"testdata/jpl_inner_event_baseline_21c_sample.json",
"basic/testdata/jpl_inner_event_baseline_21c_sample.json",
},
{
"testdata/jpl_inner_event_baseline_20c_sample.json",
"basic/testdata/jpl_inner_event_baseline_20c_sample.json",
},
{
"testdata/jpl_inner_event_baseline_22c_sample.json",
"basic/testdata/jpl_inner_event_baseline_22c_sample.json",
},
}
var merged innerBaselineFile
for index, candidates := range paths {
var (
data []byte
err error
)
for _, path := range candidates {
data, err = os.ReadFile(path)
if err == nil {
var baseline innerBaselineFile
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatal(err)
}
merged.Events = append(merged.Events, baseline.Events...)
break
}
}
if err != nil && index == 0 {
t.Fatal(err)
}
}
if len(merged.Events) == 0 {
t.Fatal("empty inner baseline file")
}
return merged
}
func parseInnerBaselineTime(t *testing.T, value string) time.Time {
t.Helper()
loc := time.FixedZone("JST", 9*3600)
layouts := []string{
"2006-01-02 15:04:05 MST",
"2006-01-02 15:04 MST",
"2006-01-02 15:04:05",
"2006-01-02 15:04",
}
var err error
for _, layout := range layouts {
when, parseErr := time.ParseInLocation(layout, value, loc)
if parseErr == nil {
return when
}
err = parseErr
}
t.Fatalf("parse baseline time %q: %v", value, err)
return time.Time{}
}
func innerBaselineTolerance(event innerBaselineEvent) time.Duration {
switch event.Kind {
case "IC", "SC", "P2R", "R2P":
return 2 * time.Minute
case "GEE", "GEW":
return 90 * time.Minute
default:
return 2 * time.Minute
}
}
func innerEventFuncs(t *testing.T, event innerBaselineEvent) (func(float64) float64, func(float64) float64) {
t.Helper()
switch event.Planet + ":" + event.Kind {
case "Mercury:IC":
return LastMercuryInferiorConjunctionInclusive, NextMercuryInferiorConjunctionInclusive
case "Mercury:SC":
return LastMercurySuperiorConjunctionInclusive, NextMercurySuperiorConjunctionInclusive
case "Mercury:P2R":
return LastMercuryProgradeToRetrogradeInclusive, NextMercuryProgradeToRetrogradeInclusive
case "Mercury:R2P":
return LastMercuryRetrogradeToProgradeInclusive, NextMercuryRetrogradeToProgradeInclusive
case "Mercury:GEE":
return LastMercuryGreatestElongationEastInclusive, NextMercuryGreatestElongationEastInclusive
case "Mercury:GEW":
return LastMercuryGreatestElongationWestInclusive, NextMercuryGreatestElongationWestInclusive
case "Venus:IC":
return LastVenusInferiorConjunctionInclusive, NextVenusInferiorConjunctionInclusive
case "Venus:SC":
return LastVenusSuperiorConjunctionInclusive, NextVenusSuperiorConjunctionInclusive
case "Venus:P2R":
return LastVenusProgradeToRetrogradeInclusive, NextVenusProgradeToRetrogradeInclusive
case "Venus:R2P":
return LastVenusRetrogradeToProgradeInclusive, NextVenusRetrogradeToProgradeInclusive
case "Venus:GEE":
return LastVenusGreatestElongationEastInclusive, NextVenusGreatestElongationEastInclusive
case "Venus:GEW":
return LastVenusGreatestElongationWestInclusive, NextVenusGreatestElongationWestInclusive
default:
t.Fatalf("unsupported event %s:%s", event.Planet, event.Kind)
return nil, nil
}
}
func assertInnerBaselineEvent(t *testing.T, event innerBaselineEvent, lastFn, nextFn func(float64) float64) {
t.Helper()
when := parseInnerBaselineTime(t, event.VerifiedJST)
before := when.Add(-24 * time.Hour)
after := when.Add(24 * time.Hour)
next := JDE2DateByZone(nextFn(toUTJD(before)), when.Location(), false)
last := JDE2DateByZone(lastFn(toUTJD(after)), when.Location(), false)
tolerance := innerBaselineTolerance(event)
if diff := next.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s next mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, next, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
if diff := last.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s last mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, last, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
}
func TestInnerPlanetTruthAgainstJPL(t *testing.T) {
baseline := loadInnerBaseline(t)
for _, event := range baseline.Events {
event := event
name := strings.Join([]string{event.Planet, event.Kind, event.VerifiedJST}, "_")
t.Run(name, func(t *testing.T) {
lastFn, nextFn := innerEventFuncs(t, event)
assertInnerBaselineEvent(t, event, lastFn, nextFn)
})
}
}
+181
View File
@@ -0,0 +1,181 @@
package basic
import (
"errors"
"math"
"time"
)
var ErrInvalidCivilDate = errors.New("invalid civil date")
var timeNow = time.Now
// Date2JDE 日期转儒略日
func Date2JDE(date time.Time) float64 {
day := float64(date.Day()) + float64(date.Hour())/24.0 + float64(date.Minute())/24.0/60.0 + float64(date.Second())/24.0/3600.0 + float64(date.Nanosecond())/1000000000.0/3600.0/24.0
return JDECalc(date.Year(), int(date.Month()), day)
}
func ValidateCivilDate(year, month int, day float64) error {
if math.IsNaN(day) || math.IsInf(day, 0) {
return ErrInvalidCivilDate
}
if month < 1 || month > 12 {
return ErrInvalidCivilDate
}
if day < 1 {
return ErrInvalidCivilDate
}
dayInt := int(math.Floor(day))
if dayInt < 1 || dayInt > daysInCivilMonth(year, month) {
return ErrInvalidCivilDate
}
if isGregorianReformGap(year, month, day) {
return ErrInvalidCivilDate
}
return nil
}
func isGregorianReformGap(year, month int, day float64) bool {
return year == 1582 && month == 10 && day >= 5 && day < 15
}
func daysInCivilMonth(year, month int) int {
switch month {
case 1, 3, 5, 7, 8, 10, 12:
return 31
case 4, 6, 9, 11:
return 30
case 2:
if isCivilLeapYear(year, month, 1) {
return 29
}
return 28
default:
return 0
}
}
func isCivilLeapYear(year, month int, day float64) bool {
if year < 1582 || (year == 1582 && (month < 10 || (month == 10 && day <= 4))) {
return year%4 == 0
}
if year%400 == 0 {
return true
}
if year%100 == 0 {
return false
}
return year%4 == 0
}
/*
@name: 儒略日计算
@dec: 计算给定时间的儒略日,1582年改力后为格里高利历,之前为儒略历
@ 请注意,传入的时间在天文计算中一般为力学时,应当注意和世界时的转化
*/
func JDECalc(year, month int, day float64) float64 {
if err := ValidateCivilDate(year, month, day); err != nil {
return math.NaN()
}
effectiveYear, effectiveMonth, effectiveDay := year, month, int(math.Floor(day))
if month == 1 || month == 2 {
year--
month += 12
}
var gregorianCorrection int
if effectiveYear < 1582 || (effectiveYear == 1582 && (effectiveMonth < 10 || (effectiveMonth == 10 && effectiveDay <= 4))) {
gregorianCorrection = 0
} else {
century := int(year / 100)
gregorianCorrection = 2 - century + int(century/4)
}
return (math.Floor(365.25*(float64(year)+4716.0)) + math.Floor(30.6001*float64(month+1)) + day + float64(gregorianCorrection) - 1524.5)
}
/*
@name: 获得当前儒略日时间:当地世界时,非格林尼治时间
*/
func GetNowJDE() (nowJDE float64) {
now := timeNow()
dayFraction := float64(now.Second())/3600.0/24.0 + float64(now.Minute())/60.0/24.0 + float64(now.Hour())/24.0
nowJDE = JDECalc(now.Year(), int(now.Month()), float64(now.Day())+dayFraction)
return
}
func JDE2Date(jd float64) time.Time {
jd = jd + 0.5
z := float64(int(jd))
f := jd - z
var a, b, years, months, days float64
if z < 2299161.0 {
a = z
} else {
alpha := math.Floor((z - 1867216.25) / 36524.25)
a = z + 1 + alpha - math.Floor(alpha/4)
}
b = a + 1524
c := math.Floor((b - 122.1) / 365.25)
d := math.Floor(365.25 * c)
e := math.Floor((b - d) / 30.6001)
days = b - d - math.Floor(30.6001*e) + f
if e < 14 {
months = e - 1
}
if e == 14 || e == 15 {
months = e - 13
}
if months > 2 {
years = c - 4716
}
if months == 1 || months == 2 {
years = c - 4715
}
tms := (days - math.Floor(days)) * 24 * 3600
days = math.Floor(days)
tz, _ := time.LoadLocation("Local")
dates := time.Date(int(years), time.Month(int(months)), int(days), 0, 0, 0, 0, tz)
return time.Unix(dates.Unix()+int64(tms), int64((tms-math.Floor(tms))*1000000000))
}
// JDE2DateByZone JDE(儒略日)转日期
// jd: 儒略日
// tz: 目标时区
// byZone: (true: 传入的儒略日视为目标时区当地时间的儒略日,false: 传入的儒略日视为UTC时间的儒略日)
// 回参:转换后的日期,时区始终为目标时区
func JDE2DateByZone(jd float64, tz *time.Location, byZone bool) time.Time {
jd = jd + 0.5
z := float64(int(jd))
f := jd - z
var a, b, years, months, days float64
if z < 2299161.0 {
a = z
} else {
alpha := math.Floor((z - 1867216.25) / 36524.25)
a = z + 1 + alpha - math.Floor(alpha/4)
}
b = a + 1524
c := math.Floor((b - 122.1) / 365.25)
d := math.Floor(365.25 * c)
e := math.Floor((b - d) / 30.6001)
days = b - d - math.Floor(30.6001*e) + f
if e < 14 {
months = e - 1
}
if e == 14 || e == 15 {
months = e - 13
}
if months > 2 {
years = c - 4716
}
if months == 1 || months == 2 {
years = c - 4715
}
tms := (days - math.Floor(days)) * 24 * 3600
days = math.Floor(days)
var transTz = tz
if !byZone {
transTz = time.UTC
}
return time.Date(int(years), time.Month(int(months)), int(days), 0, 0, 0, 0, transTz).
Add(time.Duration(int64(1000000000 * tms))).In(tz)
}
+34
View File
@@ -0,0 +1,34 @@
package basic
import (
"math"
"testing"
"time"
)
func TestGetNowJDEUsesSingleTimestamp(t *testing.T) {
oldTimeNow := timeNow
defer func() {
timeNow = oldTimeNow
}()
calls := 0
first := time.Date(2026, 4, 29, 23, 59, 59, 0, time.FixedZone("CST", 8*3600))
second := first.Add(2 * time.Second)
timeNow = func() time.Time {
calls++
if calls == 1 {
return first
}
return second
}
got := GetNowJDE()
want := Date2JDE(first)
if calls != 1 {
t.Fatalf("GetNowJDE should read current time once, got %d calls", calls)
}
if math.Float64bits(got) != math.Float64bits(want) {
t.Fatalf("GetNowJDE mismatch: got %.15f want %.15f", got, want)
}
}
+102 -341
View File
@@ -7,143 +7,112 @@ import (
. "b612.me/astro/tools"
)
func JupiterL(JD float64) float64 {
return planet.WherePlanet(4, 0, JD)
func JupiterL(jd float64) float64 {
return planet.WherePlanet(4, 0, jd)
}
func JupiterB(JD float64) float64 {
return planet.WherePlanet(4, 1, JD)
func JupiterB(jd float64) float64 {
return planet.WherePlanet(4, 1, jd)
}
func JupiterR(JD float64) float64 {
return planet.WherePlanet(4, 2, JD)
func JupiterR(jd float64) float64 {
return planet.WherePlanet(4, 2, jd)
}
func AJupiterX(JD float64) float64 {
l := JupiterL(JD)
b := JupiterB(JD)
r := JupiterR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func AJupiterX(jd float64) float64 {
l := JupiterL(jd)
b := JupiterB(jd)
r := JupiterR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x
}
func AJupiterY(JD float64) float64 {
func AJupiterY(jd float64) float64 {
l := JupiterL(JD)
b := JupiterB(JD)
r := JupiterR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
l := JupiterL(jd)
b := JupiterB(jd)
r := JupiterR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y
}
func AJupiterZ(JD float64) float64 {
//l := JupiterL(JD)
b := JupiterB(JD)
r := JupiterR(JD)
// el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func AJupiterZ(jd float64) float64 {
//l := JupiterL(jd)
b := JupiterB(jd)
r := JupiterR(jd)
// el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
z := r*Sin(b) - er*Sin(eb)
return z
}
func AJupiterXYZ(JD float64) (float64, float64, float64) {
l := JupiterL(JD)
b := JupiterB(JD)
r := JupiterR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func AJupiterXYZ(jd float64) (float64, float64, float64) {
l := JupiterL(jd)
b := JupiterB(jd)
r := JupiterR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb)
return x, y, z
}
func JupiterApparentRa(JD float64) float64 {
lo, bo := JupiterApparentLoBo(JD)
sita := Sita(JD)
ra := math.Atan2((Sin(lo)*Cos(sita) - Tan(bo)*Sin(sita)), Cos(lo))
func JupiterApparentRa(jd float64) float64 {
lo, bo := JupiterApparentLoBo(jd)
eps := TrueObliquity(jd)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
return Limit360(ra)
}
func JupiterApparentDec(JD float64) float64 {
lo, bo := JupiterApparentLoBo(JD)
sita := Sita(JD)
dec := ArcSin(Sin(bo)*Cos(sita) + Cos(bo)*Sin(sita)*Sin(lo))
func JupiterApparentDec(jd float64) float64 {
lo, bo := JupiterApparentLoBo(jd)
eps := TrueObliquity(jd)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec
}
func JupiterApparentRaDec(JD float64) (float64, float64) {
lo, bo := JupiterApparentLoBo(JD)
sita := Sita(JD)
ra := math.Atan2((Sin(lo)*Cos(sita) - Tan(bo)*Sin(sita)), Cos(lo))
func JupiterApparentRaDec(jd float64) (float64, float64) {
lo, bo := JupiterApparentLoBo(jd)
eps := TrueObliquity(jd)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
dec := ArcSin(Sin(bo)*Cos(sita) + Cos(bo)*Sin(sita)*Sin(lo))
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return Limit360(ra), dec
}
func EarthJupiterAway(JD float64) float64 {
x, y, z := AJupiterXYZ(JD)
to := math.Sqrt(x*x + y*y + z*z)
return to
func EarthJupiterAway(jd float64) float64 {
return planetEarthAwayExplicitN(4, jd, -1)
}
func JupiterApparentLo(JD float64) float64 {
x, y, z := AJupiterXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AJupiterXYZ(JD - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,JD)/3600;
//bo+=GXCBo(lo,bo,JD);
lo += Nutation2000Bi(JD)
return lo
func JupiterApparentLo(jd float64) float64 {
geo, _ := planetApparentGeocentricPositionN(4, jd, -1)
return geo.lo
}
func JupiterApparentBo(JD float64) float64 {
x, y, z := AJupiterXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AJupiterXYZ(JD - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,JD);
//bo+=GXCBo(lo,bo,JD)/3600;
//lo+=Nutation2000Bi(JD);
return bo
func JupiterApparentBo(jd float64) float64 {
geo, _ := planetApparentGeocentricPositionN(4, jd, -1)
return geo.bo
}
func JupiterApparentLoBo(JD float64) (float64, float64) {
x, y, z := AJupiterXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AJupiterXYZ(JD - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,JD)/3600;
//bo+=GXCBo(lo,bo,JD);
lo += Nutation2000Bi(JD)
return lo, bo
func JupiterApparentLoBo(jd float64) (float64, float64) {
geo, _ := planetApparentGeocentricPositionN(4, jd, -1)
return geo.lo, geo.bo
}
func JupiterMag(JD float64) float64 {
AwaySun := JupiterR(JD)
AwayEarth := EarthJupiterAway(JD)
Away := planet.WherePlanet(-1, 2, JD)
i := (AwaySun*AwaySun + AwayEarth*AwayEarth - Away*Away) / (2 * AwaySun * AwayEarth)
func JupiterMag(jd float64) float64 {
sunDistance := JupiterR(jd)
earthDistance := EarthJupiterAway(jd)
earthSunDistance := planet.WherePlanet(-1, 2, jd)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(i)
Mag := -9.40 + 5*math.Log10(AwaySun*AwayEarth) + 0.0005*i
return FloatRound(Mag, 2)
mag := -9.40 + 5*math.Log10(sunDistance*earthDistance) + 0.0005*i
return FloatRound(mag, 2)
}
func JupiterHeight(jde, lon, lat, timezone float64) float64 {
@@ -153,10 +122,10 @@ func JupiterHeight(jde, lon, lat, timezone float64) float64 {
ra, dec := JupiterApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
// 计算时角
H := Limit360(st - ra)
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(H)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(H)
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(hourAngle)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(hourAngle)
return ArcSin(sinHeight)
}
@@ -167,271 +136,63 @@ func JupiterAzimuth(jde, lon, lat, timezone float64) float64 {
ra, dec := JupiterApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
// 计算时角
H := Limit360(st - ra)
hourAngle := Limit360(st - ra)
// 三角转换公式
tanAzimuth := Sin(H) / (Cos(H)*Sin(lat) - Tan(dec)*Cos(lat))
Azimuth := ArcTan(tanAzimuth)
if Azimuth < 0 {
if H/15 < 12 {
return Azimuth + 360
tanAzimuth := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(dec)*Cos(lat))
azimuth := ArcTan(tanAzimuth)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
}
return Azimuth + 180
return azimuth + 180
}
if H/15 < 12 {
return Azimuth + 180
if hourAngle/15 < 12 {
return azimuth + 180
}
return Azimuth
return azimuth
}
func JupiterHourAngle(JD, Lon, TZ float64) float64 {
startime := Limit360(ApparentSiderealTime(JD-TZ/24)*15 + Lon)
timeangle := startime - JupiterApparentRa(TD2UT(JD-TZ/24.0, true))
if timeangle < 0 {
timeangle += 360
func JupiterHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon)
hourAngle := siderealLongitude - JupiterApparentRa(TD2UT(jd-timezone/24.0, true))
if hourAngle < 0 {
hourAngle += 360
}
return timeangle
return hourAngle
}
func JupiterCulminationTime(jde, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5
JD1 := jde + Limit360(360-JupiterHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
limitHA := func(jde, lon, timezone float64) float64 {
ha := JupiterHourAngle(jde, lon, timezone)
if ha < 180 {
ha += 360
estimateJD := jde + Limit360(360-JupiterHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 {
currentHourAngle := JupiterHourAngle(jde, lon, timezone)
if currentHourAngle < 180 {
currentHourAngle += 360
}
return ha
return currentHourAngle
}
for {
JD0 := JD1
stDegree := limitHA(JD0, lon, timezone) - 360
stDegreep := (limitHA(JD0+0.000005, lon, timezone) - limitHA(JD0-0.000005, lon, timezone)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
prevJD := estimateJD
hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360
hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001
estimateJD = prevJD - hourAngleDelta/hourAngleSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return JD1
return estimateJD
}
func JupiterRiseTime(JD, Lon, Lat, TZ, ZS, HEI float64) float64 {
return jupiterRiseDown(JD, Lon, Lat, TZ, ZS, HEI, true)
func JupiterRiseTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return jupiterRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true)
}
func JupiterDownTime(JD, Lon, Lat, TZ, ZS, HEI float64) float64 {
return jupiterRiseDown(JD, Lon, Lat, TZ, ZS, HEI, false)
func JupiterSetTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return jupiterRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false)
}
func jupiterRiseDown(JD, Lon, Lat, TZ, ZS, HEI float64, isRise bool) float64 {
var An float64
JD = math.Floor(JD) + 0.5
ntz := math.Round(Lon / 15)
if ZS != 0 {
An = -0.8333
}
An = An - HeightDegreeByLat(HEI, Lat)
tztime := JupiterCulminationTime(JD, Lon, ntz)
if JupiterHeight(tztime, Lon, Lat, ntz) < An {
return -2 //极夜
}
if JupiterHeight(tztime-0.5, Lon, Lat, ntz) > An {
return -1 //极昼
}
dec := HSunApparentDec(TD2UT(tztime-ntz/24, true))
//(sin(ho)-sin(φ)*sin(δ2))/(cos(φ)*cos(δ2))
tmp := (Sin(An) - Sin(dec)*Sin(Lat)) / (Cos(dec) * Cos(Lat))
var rise float64
if math.Abs(tmp) <= 1 {
rzsc := ArcCos(tmp) / 15
if isRise {
rise = tztime - rzsc/24 - 25.0/24.0/60.0
} else {
rise = tztime + rzsc/24 - 25.0/24.0/60.0
}
} else {
rise = tztime
i := 0
//TODO:使用二分法计算
for JupiterHeight(rise, Lon, Lat, ntz) > An {
i++
if isRise {
rise -= 15.0 / 60.0 / 24.0
} else {
rise += 15.0 / 60.0 / 24.0
}
if i > 48 {
break
}
}
}
JD1 := rise
for {
JD0 := JD1
stDegree := JupiterHeight(JD0, Lon, Lat, ntz) - An
stDegreep := (JupiterHeight(JD0+0.000005, Lon, Lat, ntz) - JupiterHeight(JD0-0.000005, Lon, Lat, ntz)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
break
}
}
return JD1 - ntz/24 + TZ/24
}
// Pos
const JUPITER_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 4332.59))
func jupiterConjunction(jde, degree float64, next uint8) float64 {
//0=last 1=next
decSub := func(jde float64, degree float64, filter bool) float64 {
sub := Limit360(Limit360(JupiterApparentLo(jde)-HSunApparentLo(jde)) - degree)
if filter {
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
}
return sub
}
dayCost := JUPITER_S_PERIOD / 360
nowSub := decSub(jde, degree, false)
if next == 0 {
jde -= (360 - nowSub) * dayCost
} else {
jde += dayCost * nowSub
}
JD1 := jde
for {
JD0 := JD1
stDegree := decSub(JD0, degree, true)
stDegreep := (decSub(JD0+0.000005, degree, true) - decSub(JD0-0.000005, degree, true)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
break
}
}
return TD2UT(JD1, false)
}
func LastJupiterConjunction(jde float64) float64 {
return jupiterConjunction(jde, 0, 0)
}
func NextJupiterConjunction(jde float64) float64 {
return jupiterConjunction(jde, 0, 1)
}
func LastJupiterOpposition(jde float64) float64 {
return jupiterConjunction(jde, 180, 0)
}
func NextJupiterOpposition(jde float64) float64 {
return jupiterConjunction(jde, 180, 1)
}
func NextJupiterEasternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 90, 1)
}
func LastJupiterEasternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 90, 0)
}
func NextJupiterWesternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 270, 1)
}
func LastJupiterWesternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 270, 0)
}
func jupiterRetrograde(jde float64, isLeft bool) float64 {
//0=last 1=next
decSub := func(jde float64, val float64) float64 {
sub := JupiterApparentRa(jde+val) - JupiterApparentRa(jde-val)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * val)
}
jde = NextJupiterOpposition(jde)
if isLeft {
jde -= 60
} else {
jde += 60
}
for {
nowSub := decSub(jde, 1.0/86400.0)
if math.Abs(nowSub) > 0.55 {
jde += 2
continue
}
break
}
JD1 := jde
for {
JD0 := JD1
stDegree := decSub(JD0, 2.0/86400.0)
stDegreep := (decSub(JD0+15.0/86400.0, 2.0/86400.0) - decSub(JD0-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 30.0/86400.0 {
break
}
}
JD1 = JD1 - 15.0/86400.0
min := JD1
minRa := 100.0
for i := 0.0; i < 60.0; i++ {
tmp := decSub(JD1+i*0.5/86400.0, 0.5/86400.0)
if math.Abs(tmp) < math.Abs(minRa) {
minRa = tmp
min = JD1 + i*0.5/86400.0
}
}
return TD2UT(min, false)
}
func NextJupiterRetrogradeToPrograde(jde float64) float64 {
date := jupiterRetrograde(jde, false)
if date < jde {
op := NextJupiterOpposition(jde)
return jupiterRetrograde(op+10, false)
}
return date
}
func LastJupiterRetrogradeToPrograde(jde float64) float64 {
jde = LastJupiterOpposition(jde) - 10
date := jupiterRetrograde(jde, false)
if date > jde {
op := LastJupiterOpposition(jde)
return jupiterRetrograde(op-10, false)
}
return date
}
func NextJupiterProgradeToRetrograde(jde float64) float64 {
date := jupiterRetrograde(jde, true)
if date < jde {
op := NextJupiterOpposition(jde)
return jupiterRetrograde(op+10, true)
}
return date
}
func LastJupiterProgradeToRetrograde(jde float64) float64 {
jde = LastJupiterOpposition(jde) - 10
date := jupiterRetrograde(jde, true)
if date > jde {
op := LastJupiterOpposition(jde)
return jupiterRetrograde(op-10, true)
}
return date
func jupiterRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool) (float64, error) {
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, JupiterCulminationTime, JupiterHeight, JupiterApparentDec)
}
+207
View File
@@ -0,0 +1,207 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
// Pos
const (
JUPITER_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 4332.59))
jupiterEventSearchN = 16
jupiterPhaseCoarseTolerance = 30.0 / 86400.0
)
func jupiterSunLongitudeDelta(jde, degree float64, filter bool) float64 {
sub := Limit360(Limit360(JupiterApparentLo(jde)-HSunApparentLo(jde)) - degree)
if filter {
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
}
return sub
}
func jupiterSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64 {
sub := Limit360(Limit360(JupiterApparentLoN(jde, n)-HSunApparentLoN(jde, n)) - degree)
if filter {
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
}
return sub
}
func jupiterRADerivative(jde, delta float64) float64 {
sub := JupiterApparentRa(jde+delta) - JupiterApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func jupiterRADerivativeN(jde, delta float64, n int) float64 {
sub := JupiterApparentRaN(jde+delta, n) - JupiterApparentRaN(jde-delta, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func jupiterConjunctionFull(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := JUPITER_S_PERIOD / 360
currentDelta := jupiterSunLongitudeDelta(jde, degree, false)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
for {
prevJD := estimateJD
longitudeDelta := jupiterSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJD+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
estimateJD = prevJD - longitudeDelta/longitudeSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return TD2UT(estimateJD, false)
}
func jupiterConjunction(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := JUPITER_S_PERIOD / 360
currentDelta := jupiterSunLongitudeDelta(jde, degree, false)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
for {
prevJD := estimateJD
longitudeDelta := jupiterSunLongitudeDeltaN(prevJD, degree, true, jupiterEventSearchN)
longitudeSlope := (jupiterSunLongitudeDeltaN(prevJD+0.000005, degree, true, jupiterEventSearchN) - jupiterSunLongitudeDeltaN(prevJD-0.000005, degree, true, jupiterEventSearchN)) / 0.00001
estimateJD = prevJD - longitudeDelta/longitudeSlope
if math.Abs(estimateJD-prevJD) <= jupiterPhaseCoarseTolerance {
break
}
}
for {
prevJD := estimateJD
longitudeDelta := jupiterSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJD+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
estimateJD = prevJD - longitudeDelta/longitudeSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return TD2UT(estimateJD, false)
}
func LastJupiterConjunction(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 0, jupiterConjunction)
}
func NextJupiterConjunction(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 0, jupiterConjunction)
}
func LastJupiterOpposition(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 180, jupiterConjunction)
}
func NextJupiterOpposition(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 180, jupiterConjunction)
}
func NextJupiterEasternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 90, jupiterConjunction)
}
func LastJupiterEasternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 90, jupiterConjunction)
}
func NextJupiterWesternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 270, jupiterConjunction)
}
func LastJupiterWesternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 270, jupiterConjunction)
}
func jupiterRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
oppositionTT := TD2UT(oppositionJD, true)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
easternQuadratureUT := jupiterConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
} else {
westernQuadratureUT := jupiterConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true)
}
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return jupiterRADerivativeN(jd, 1.0/86400.0, jupiterEventSearchN)
}, func(jd float64) float64 {
return jupiterRADerivative(jd, 0.5/86400.0)
})
return TD2UT(bestJD, false)
}
func NextJupiterRetrogradeToPrograde(jde float64) float64 {
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
return jupiterRetrogradeAroundOpposition(nextOppositionJD, false)
}
func LastJupiterRetrogradeToPrograde(jde float64) float64 {
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
previousOppositionJD := jupiterConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return jupiterRetrogradeAroundOpposition(previousOppositionJD, false)
}
func NextJupiterProgradeToRetrograde(jde float64) float64 {
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
followingOppositionJD := jupiterConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return jupiterRetrogradeAroundOpposition(followingOppositionJD, true)
}
func LastJupiterProgradeToRetrograde(jde float64) float64 {
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
return jupiterRetrogradeAroundOpposition(lastOppositionJD, true)
}
+120
View File
@@ -0,0 +1,120 @@
package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
type jupiterPhysicalObservationInfo struct {
DS float64
DE float64
SystemI float64
SystemII float64
}
// JupiterCentralMeridianInfo 木星中央经线 / Jupiter central meridians.
type JupiterCentralMeridianInfo struct {
// SystemI 木星 System I 照亮盘中央经线,单位度,西经为正。
SystemI float64
// SystemII 木星 System II 照亮盘中央经线,单位度,西经为正。
SystemII float64
// SystemIII 木星 System III 盘面中央经线,单位度,西经为正。
SystemIII float64
}
// JupiterCentralMeridians 木星 System I/II/III 中央经线 / Jupiter System I/II/III central meridians.
func JupiterCentralMeridians(jd float64) JupiterCentralMeridianInfo {
return JupiterCentralMeridiansN(jd, -1)
}
// JupiterCentralMeridiansN 木星 System I/II/III 中央经线(截断版) / truncated Jupiter System I/II/III central meridians.
func JupiterCentralMeridiansN(jd float64, n int) JupiterCentralMeridianInfo {
observations := jupiterPhysicalObservationsN(jd, n)
physical := JupiterPhysicalN(jd, n)
return JupiterCentralMeridianInfo{
SystemI: observations.SystemI,
SystemII: observations.SystemII,
SystemIII: physical.SubEarthLongitude,
}
}
// JupiterDSDE 木星 DS/DE 行星中心赤纬 / Jupiter planetocentric declinations of Sun and Earth.
func JupiterDSDE(jd float64) (ds, de float64) {
return JupiterDSDEN(jd, -1)
}
// JupiterDSDEN 木星 DS/DE 行星中心赤纬(截断版) / truncated Jupiter planetocentric declinations of Sun and Earth.
func JupiterDSDEN(jd float64, n int) (ds, de float64) {
observations := jupiterPhysicalObservationsN(jd, n)
return observations.DS, observations.DE
}
func jupiterPhysicalObservationsN(jd float64, n int) jupiterPhysicalObservationInfo {
days := jd - 2433282.5
julianCentury := days / 36525.0
poleRA := (268.0 + 0.1061*julianCentury) * rad
poleDec := (64.5 - 0.0164*julianCentury) * rad
w1 := (17.71 + 877.90003539*days) * rad
w2 := (16.838 + 870.27003539*days) * rad
earthLon := planet.WherePlanetN(-1, 0, jd, n)
earthLat := planet.WherePlanetN(-1, 1, jd, n)
earthRadius := planet.WherePlanetN(-1, 2, jd, n)
delta := 4.0
var jupiterLon float64
var jupiterLat float64
var jupiterRadius float64
var x float64
var y float64
var z float64
for i := 0; i < 2; i++ {
lightTimeDays := astronomicalUnitLightTimeDays * delta
jupiterLon = planet.WherePlanetN(4, 0, jd-lightTimeDays, n)
jupiterLat = planet.WherePlanetN(4, 1, jd-lightTimeDays, n)
jupiterRadius = planet.WherePlanetN(4, 2, jd-lightTimeDays, n)
x = jupiterRadius*Cos(jupiterLat)*Cos(jupiterLon) - earthRadius*Cos(earthLat)*Cos(earthLon)
y = jupiterRadius*Cos(jupiterLat)*Sin(jupiterLon) - earthRadius*Cos(earthLat)*Sin(earthLon)
z = jupiterRadius*Sin(jupiterLat) - earthRadius*Sin(earthLat)
delta = math.Sqrt(x*x + y*y + z*z)
}
meanObliquity := EclipticObliquity(jd, false)
sinMeanObliquity, cosMeanObliquity := math.Sincos(meanObliquity)
sinJupiterLat, cosJupiterLat := math.Sincos(jupiterLat * rad)
sinJupiterLon, cosJupiterLon := math.Sincos(jupiterLon * rad)
alphaSun := math.Atan2(cosMeanObliquity*sinJupiterLon-sinMeanObliquity*sinJupiterLat/cosJupiterLat, cosJupiterLon)
deltaSun := math.Asin(cosMeanObliquity*sinJupiterLat + sinMeanObliquity*cosJupiterLat*sinJupiterLon)
u := y*Cos(meanObliquity) - z*Sin(meanObliquity)
v := y*Sin(meanObliquity) + z*Cos(meanObliquity)
alpha := math.Atan2(u, x)
deltaEarth := math.Atan2(v, math.Hypot(x, u))
sinPoleDec, cosPoleDec := math.Sincos(poleDec)
sinDeltaSun, cosDeltaSun := math.Sincos(deltaSun)
ds := math.Asin(-sinPoleDec*sinDeltaSun-cosPoleDec*cosDeltaSun*math.Cos(poleRA-alphaSun)) * deg
sinDeltaEarth, cosDeltaEarth := math.Sincos(deltaEarth)
sinPoleDeltaRA, cosPoleDeltaRA := math.Sincos(poleRA - alpha)
zeta := math.Atan2(
sinPoleDec*cosDeltaEarth*cosPoleDeltaRA-sinDeltaEarth*cosPoleDec,
cosDeltaEarth*sinPoleDeltaRA,
)
de := math.Asin(-sinPoleDec*sinDeltaEarth-cosPoleDec*cosDeltaEarth*math.Cos(poleRA-alpha)) * deg
systemI := w1 - zeta - 5.07033*rad*delta
systemII := w2 - zeta - 5.02626*rad*delta
phaseCorrection := (2*jupiterRadius*delta + earthRadius*earthRadius - jupiterRadius*jupiterRadius - delta*delta) / (4 * jupiterRadius * delta)
if Sin(jupiterLon-earthLon) < 0 {
phaseCorrection = -phaseCorrection
}
return jupiterPhysicalObservationInfo{
DS: ds,
DE: de,
SystemI: Limit360((systemI + phaseCorrection) * deg),
SystemII: Limit360((systemII + phaseCorrection) * deg),
}
}
+92
View File
@@ -0,0 +1,92 @@
package basic
import (
"encoding/json"
"math"
"os"
"testing"
"time"
)
func TestJupiterCentralMeridiansMeeusExample43A(t *testing.T) {
got := JupiterCentralMeridians(2448972.50068)
ds, de := JupiterDSDE(2448972.50068)
assertPlanetPhaseClose(t, "Jupiter.CMI", got.SystemI, 268.06, 0.02)
assertPlanetPhaseClose(t, "Jupiter.CMII", got.SystemII, 72.74, 0.02)
assertPlanetPhaseClose(t, "Jupiter.DS.Exact", ds, -1.733360091891, 1e-9)
assertPlanetPhaseClose(t, "Jupiter.DE.Exact", de, -2.484625891057, 1e-9)
}
func TestJupiterCentralMeridianSystemIIIMatchesHorizonsBaseline(t *testing.T) {
data, err := os.ReadFile("testdata/planet_physical_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []planetPhysicalSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
for _, sample := range samples {
if sample.Body != "jupiter" {
continue
}
date, err := time.Parse(time.RFC3339, sample.InputUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.InputUTC, err)
}
jd := TD2UT(Date2JDE(date.UTC()), true)
got := JupiterCentralMeridians(jd)
assertPlanetPhaseClose(t, "Jupiter."+sample.InputUTC+".CMIII", got.SystemIII, sample.SubEarthLongitude, 0.02)
}
}
func TestJupiterCentralMeridiansNFullMatchesDefault(t *testing.T) {
jd := TD2UT(Date2JDE(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)), true)
got := JupiterCentralMeridians(jd)
gotN := JupiterCentralMeridiansN(jd, -1)
ds, de := JupiterDSDE(jd)
dsN, deN := JupiterDSDEN(jd, -1)
if math.Float64bits(got.SystemI) != math.Float64bits(gotN.SystemI) {
t.Fatalf("SystemI mismatch: got %.18f want %.18f", got.SystemI, gotN.SystemI)
}
if math.Float64bits(got.SystemII) != math.Float64bits(gotN.SystemII) {
t.Fatalf("SystemII mismatch: got %.18f want %.18f", got.SystemII, gotN.SystemII)
}
if math.Float64bits(got.SystemIII) != math.Float64bits(gotN.SystemIII) {
t.Fatalf("SystemIII mismatch: got %.18f want %.18f", got.SystemIII, gotN.SystemIII)
}
if math.Float64bits(ds) != math.Float64bits(dsN) {
t.Fatalf("DS mismatch: got %.18f want %.18f", ds, dsN)
}
if math.Float64bits(de) != math.Float64bits(deN) {
t.Fatalf("DE mismatch: got %.18f want %.18f", de, deN)
}
}
func TestJupiterCentralMeridianAndDSDESampleSweepFiniteAndInRange(t *testing.T) {
dates := []time.Time{
time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC),
time.Date(1969, 7, 20, 20, 17, 40, 0, time.UTC),
time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC),
time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC),
time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC),
}
for _, date := range dates {
jd := TD2UT(Date2JDE(date.UTC()), true)
meridians := JupiterCentralMeridians(jd)
ds, de := JupiterDSDE(jd)
physical := JupiterPhysical(jd)
prefix := date.Format(time.RFC3339)
assertFiniteRange(t, prefix+".SystemI", meridians.SystemI, 0, 360, true)
assertFiniteRange(t, prefix+".SystemII", meridians.SystemII, 0, 360, true)
assertFiniteRange(t, prefix+".SystemIII", meridians.SystemIII, 0, 360, true)
assertFiniteRange(t, prefix+".DS", ds, -90, 90, false)
assertFiniteRange(t, prefix+".DE", de, -90, 90, false)
assertSameFloat(t, prefix+".CMIII", meridians.SystemIII, physical.SubEarthLongitude)
}
}
+827
View File
@@ -0,0 +1,827 @@
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(),
}
}
@@ -0,0 +1,101 @@
package basic
import (
"math"
"testing"
"time"
)
func TestJupiterGalileanPhenomenonContactEventsAgainstIMCCEBaseline(t *testing.T) {
records := loadGalileanEventBaseline(t)
maxStartDiff := 0.0
maxEndDiff := 0.0
maxStartDurationDiff := 0.0
maxEndDurationDiff := 0.0
for _, record := range records {
startUTC := mustParseRFC3339Nano(t, record.StartUTC)
endUTC := mustParseRFC3339Nano(t, record.EndUTC)
queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2)
phenomenonType := parseBasicGalileanPhenomenonType(t, record.Type)
event := ClosestJupiterGalileanPhenomenonContactEvent(Date2JDE(queryMid.UTC()), record.Satellite, phenomenonType)
if !event.Valid {
t.Fatalf("%s invalid contact event", record.Label)
}
gotStart := JDE2DateByZone(event.Disappearance.Start, time.UTC, false)
gotEnd := JDE2DateByZone(event.Reappearance.Start, time.UTC, false)
startDiff := math.Abs(gotStart.Sub(startUTC).Seconds())
endDiff := math.Abs(gotEnd.Sub(endUTC).Seconds())
startDurationDiff := math.Abs((event.Disappearance.End-event.Disappearance.Start)*86400 - record.StartDurationMinutes*60)
endDurationDiff := math.Abs((event.Reappearance.End-event.Reappearance.Start)*86400 - record.EndDurationMinutes*60)
if startDiff > maxStartDiff {
maxStartDiff = startDiff
}
if endDiff > maxEndDiff {
maxEndDiff = endDiff
}
if startDurationDiff > maxStartDurationDiff {
maxStartDurationDiff = startDurationDiff
}
if endDurationDiff > maxEndDurationDiff {
maxEndDurationDiff = endDurationDiff
}
if startDiff > galileanContactTimeToleranceSeconds(phenomenonType) {
t.Fatalf("%s disappearance start mismatch: got %s want %s", record.Label, gotStart.Format(time.RFC3339Nano), startUTC.Format(time.RFC3339Nano))
}
if endDiff > galileanContactTimeToleranceSeconds(phenomenonType) {
t.Fatalf("%s reappearance start mismatch: got %s want %s", record.Label, gotEnd.Format(time.RFC3339Nano), endUTC.Format(time.RFC3339Nano))
}
if startDurationDiff > galileanContactDurationToleranceSeconds(phenomenonType) {
t.Fatalf("%s disappearance duration mismatch: got %.1fs want %.1fs", record.Label, (event.Disappearance.End-event.Disappearance.Start)*86400, record.StartDurationMinutes*60)
}
if endDurationDiff > galileanContactDurationToleranceSeconds(phenomenonType) {
t.Fatalf("%s reappearance duration mismatch: got %.1fs want %.1fs", record.Label, (event.Reappearance.End-event.Reappearance.Start)*86400, record.EndDurationMinutes*60)
}
if !(event.Disappearance.Start <= event.Disappearance.ModelCrossing && event.Disappearance.ModelCrossing <= event.Disappearance.End) {
t.Fatalf("%s contact ordering invalid", record.Label)
}
if !(event.Reappearance.Start <= event.Reappearance.ModelCrossing && event.Reappearance.ModelCrossing <= event.Reappearance.End) {
t.Fatalf("%s reappearance ordering invalid", record.Label)
}
fullEvent := ClosestJupiterGalileanPhenomenonEvent(Date2JDE(queryMid.UTC()), record.Satellite, phenomenonType)
if phenomenonType != JupiterGalileanShadowTransit {
if math.Abs(event.Disappearance.ModelCrossing-fullEvent.Start)*86400 > 2 {
t.Fatalf("%s disappearance model crossing mismatch", record.Label)
}
if math.Abs(event.Reappearance.ModelCrossing-fullEvent.End)*86400 > 2 {
t.Fatalf("%s reappearance model crossing mismatch", record.Label)
}
}
}
t.Logf(
"galilean contact baseline max diff: start=%.1fs end=%.1fs startDur=%.1fs endDur=%.1fs",
maxStartDiff,
maxEndDiff,
maxStartDurationDiff,
maxEndDurationDiff,
)
}
func galileanContactTimeToleranceSeconds(phenomenonType JupiterGalileanPhenomenonType) float64 {
switch phenomenonType {
case JupiterGalileanShadowTransit:
return 90.0
case JupiterGalileanEclipse:
return 180.0
default:
return 120.0
}
}
func galileanContactDurationToleranceSeconds(phenomenonType JupiterGalileanPhenomenonType) float64 {
switch phenomenonType {
case JupiterGalileanShadowTransit:
return 15.0
default:
return 90.0
}
}
+498
View File
@@ -0,0 +1,498 @@
package basic
import "math"
const (
jupiterGalileanEventSearchSpanDays = 8 * 365.25
jupiterGalileanEventEpsilonDays = 1.0 / 86400.0
jupiterGalileanBoundaryStepDays = 1.0 / 24.0
)
// JupiterGalileanPhenomenonType 伽利略卫星现象类型 / Galilean-satellite phenomenon type.
type JupiterGalileanPhenomenonType string
const (
// JupiterGalileanTransit 凌日 / satellite transit across Jupiter.
JupiterGalileanTransit JupiterGalileanPhenomenonType = "transit"
// JupiterGalileanOccultation 掩蔽 / occultation behind Jupiter.
JupiterGalileanOccultation JupiterGalileanPhenomenonType = "occultation"
// JupiterGalileanEclipse 食 / eclipse in Jupiter's shadow.
JupiterGalileanEclipse JupiterGalileanPhenomenonType = "eclipse"
// JupiterGalileanShadowTransit 影凌 / shadow transit across Jupiter.
JupiterGalileanShadowTransit JupiterGalileanPhenomenonType = "shadow_transit"
)
// JupiterGalileanPhenomenonEvent 伽利略卫星现象整场事件 / full Galilean-satellite phenomenon event.
//
// Start、Greatest、End 都使用 UTC/UT 对应的儒略日。
// Start, Greatest, and End are UTC/UT Julian days.
type JupiterGalileanPhenomenonEvent struct {
Valid bool
Satellite int
Type JupiterGalileanPhenomenonType
Start float64
Greatest float64
End float64
GreatestPhenomenon JupiterGalileanPhenomenon
}
type jupiterGalileanMetricSample struct {
active bool
metric float64
phenomenon JupiterGalileanPhenomenon
}
type jupiterGalileanShadowPoint struct {
hasIntersection bool
visible bool
pathLengthAU float64
xAU float64
yAU float64
xJupiterRadii float64
yJupiterRadii float64
}
// LastJupiterGalileanPhenomenonEvent 上一次伽利略卫星现象 / previous Galilean-satellite event.
func LastJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
return event
}
// NextJupiterGalileanPhenomenonEvent 下一次伽利略卫星现象 / next Galilean-satellite event.
func NextJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
return event
}
// ClosestJupiterGalileanPhenomenonEvent 最近一次伽利略卫星现象 / closest Galilean-satellite event.
func ClosestJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
last, hasLast := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
next, hasNext := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
switch {
case hasLast && !hasNext:
return last
case !hasLast && hasNext:
return next
case !hasLast && !hasNext:
return invalidJupiterGalileanPhenomenonEvent()
}
if math.Abs(last.Greatest-jd) <= math.Abs(next.Greatest-jd) {
return last
}
return next
}
func searchJupiterGalileanPhenomenonEvent(
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
direction int,
includeCurrent bool,
) (JupiterGalileanPhenomenonEvent, bool) {
if !isFinite(jd) || direction == 0 || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
return invalidJupiterGalileanPhenomenonEvent(), false
}
if sample := jupiterGalileanPhenomenonMetricAt(jd, satellite, phenomenonType); sample.active {
current := findJupiterGalileanPhenomenonEventAround(jd, satellite, phenomenonType)
if current.Valid && includeCurrent {
return current, true
}
if current.Valid {
if direction > 0 {
jd = current.End + jupiterGalileanEventEpsilonDays
} else {
jd = current.Start - jupiterGalileanEventEpsilonDays
}
}
}
stepDays := jupiterGalileanCoarseStepDays(satellite)
maxSteps := int(math.Ceil(jupiterGalileanEventSearchSpanDays / stepDays))
sign := float64(direction)
prevTime := jd
prevSample := jupiterGalileanPhenomenonMetricAt(prevTime, satellite, phenomenonType)
midTime := jd + sign*stepDays
midSample := jupiterGalileanPhenomenonMetricAt(midTime, satellite, phenomenonType)
for i := 2; i <= maxSteps; i++ {
nextTime := jd + sign*float64(i)*stepDays
nextSample := jupiterGalileanPhenomenonMetricAt(nextTime, satellite, phenomenonType)
if isFinite(midSample.metric) &&
midSample.metric <= prevSample.metric &&
midSample.metric <= nextSample.metric {
candidate := refineJupiterGalileanMetricMinimum(prevTime, nextTime, satellite, phenomenonType)
event := findJupiterGalileanPhenomenonEventAround(candidate, satellite, phenomenonType)
if event.Valid && jupiterGalileanEventMatchesDirection(event.Greatest, jd, direction, includeCurrent) {
return event, true
}
}
prevTime, prevSample = midTime, midSample
midTime, midSample = nextTime, nextSample
}
return invalidJupiterGalileanPhenomenonEvent(), false
}
func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
sample := jupiterGalileanPhenomenonMetricAt(jd, satellite, phenomenonType)
if !sample.active {
return invalidJupiterGalileanPhenomenonEvent()
}
stepDays := jupiterGalileanBoundaryStep(satellite)
maxBoundarySteps := int(math.Ceil(jupiterGalileanOrbitPeriodDays(satellite)/stepDays)) + 4
activeStart := jd
inactiveStart := math.NaN()
for i := 0; i < maxBoundarySteps; i++ {
candidate := activeStart - stepDays
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType).active {
inactiveStart = candidate
break
}
activeStart = candidate
}
if !isFinite(inactiveStart) {
return invalidJupiterGalileanPhenomenonEvent()
}
activeEnd := jd
inactiveEnd := math.NaN()
for i := 0; i < maxBoundarySteps; i++ {
candidate := activeEnd + stepDays
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType).active {
inactiveEnd = candidate
break
}
activeEnd = candidate
}
if !isFinite(inactiveEnd) {
return invalidJupiterGalileanPhenomenonEvent()
}
start := refineJupiterGalileanEventStart(inactiveStart, activeStart, satellite, phenomenonType)
end := refineJupiterGalileanEventEnd(activeEnd, inactiveEnd, satellite, phenomenonType)
if !isFinite(start) || !isFinite(end) || end <= start {
return invalidJupiterGalileanPhenomenonEvent()
}
greatest := refineJupiterGalileanMetricMinimum(start, end, satellite, phenomenonType)
greatestSample := jupiterGalileanPhenomenonMetricAt(greatest, satellite, phenomenonType)
if !greatestSample.active {
return invalidJupiterGalileanPhenomenonEvent()
}
return JupiterGalileanPhenomenonEvent{
Valid: true,
Satellite: satellite,
Type: phenomenonType,
Start: start,
Greatest: greatest,
End: end,
GreatestPhenomenon: greatestSample.phenomenon,
}
}
func refineJupiterGalileanEventStart(
outsideJD, insideJD float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
if insideJD < outsideJD {
outsideJD, insideJD = insideJD, outsideJD
}
if jupiterGalileanPhenomenonMetricAt(outsideJD, satellite, phenomenonType).active {
return math.NaN()
}
if !jupiterGalileanPhenomenonMetricAt(insideJD, satellite, phenomenonType).active {
return math.NaN()
}
left := outsideJD
right := insideJD
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
mid := (left + right) / 2
if jupiterGalileanPhenomenonMetricAt(mid, satellite, phenomenonType).active {
right = mid
} else {
left = mid
}
}
return right
}
func refineJupiterGalileanEventEnd(
insideJD, outsideJD float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
if outsideJD < insideJD {
insideJD, outsideJD = outsideJD, insideJD
}
if !jupiterGalileanPhenomenonMetricAt(insideJD, satellite, phenomenonType).active {
return math.NaN()
}
if jupiterGalileanPhenomenonMetricAt(outsideJD, satellite, phenomenonType).active {
return math.NaN()
}
left := insideJD
right := outsideJD
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
mid := (left + right) / 2
if jupiterGalileanPhenomenonMetricAt(mid, satellite, phenomenonType).active {
left = mid
} else {
right = mid
}
}
return left
}
func refineJupiterGalileanMetricMinimum(
jd1, jd2 float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
left := math.Min(jd1, jd2)
right := math.Max(jd1, jd2)
if right-left <= jupiterGalileanEventEpsilonDays {
return (left + right) / 2
}
const phi = 0.6180339887498948482
x1 := right - phi*(right-left)
x2 := left + phi*(right-left)
f1 := jupiterGalileanPhenomenonMetricAt(x1, satellite, phenomenonType).metric
f2 := jupiterGalileanPhenomenonMetricAt(x2, satellite, phenomenonType).metric
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
if f1 <= f2 {
right = x2
x2 = x1
f2 = f1
x1 = right - phi*(right-left)
f1 = jupiterGalileanPhenomenonMetricAt(x1, satellite, phenomenonType).metric
} else {
left = x1
x1 = x2
f1 = f2
x2 = left + phi*(right-left)
f2 = jupiterGalileanPhenomenonMetricAt(x2, satellite, phenomenonType).metric
}
}
return (left + right) / 2
}
func jupiterGalileanPhenomenonMetricAt(
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) jupiterGalileanMetricSample {
if !isFinite(jd) || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
return jupiterGalileanMetricSample{
metric: math.Inf(1),
phenomenon: invalidJupiterGalileanPhenomenon(),
}
}
evaluationJD := TD2UT(jd, true)
context := newJupiterGalileanObservationContext(evaluationJD)
if context.jupiterDistance == 0 {
return jupiterGalileanMetricSample{
metric: math.Inf(1),
phenomenon: invalidJupiterGalileanPhenomenon(),
}
}
index := satellite - 1
observation := context.observationForSatellite(index)
stateVector := Vector3{observation.State.X, observation.State.Y, observation.State.Z}
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
xEarth := observation.OffsetXJupiterRadii
yEarth := observation.OffsetYJupiterRadii
earthMetric := ellipseMetric(xEarth, yEarth, 1, context.earthMinorRadius)
onEarthDisk := ellipseInside(xEarth, yEarth, 1, context.earthMinorRadius)
xSunAU := vectorDot(stateVector, context.sunEast)
ySunAU := vectorDot(stateVector, context.sunNorth)
zSunAU := vectorDot(stateVector, context.sunLineOfSight)
xSun := xSunAU / radiusAU
ySun := ySunAU / radiusAU
umbraScale := jupiterUmbraScale(zSunAU, context.sunDistanceAU)
sunMetric := math.Inf(1)
eclipse := false
if umbraScale > 0 {
sunMetric = ellipseMetric(xSun, ySun, umbraScale, context.sunMinorRadius*umbraScale)
eclipse = zSunAU > 0 && sunMetric <= 1+1e-12
}
shadowPoint := context.shadowPointFor(stateVector)
shadowMetric := math.Inf(1)
shadowTransit := false
if shadowPoint.hasIntersection {
shadowMetric = ellipseMetric(shadowPoint.xJupiterRadii, shadowPoint.yJupiterRadii, 1, context.earthMinorRadius)
shadowTransit = shadowPoint.visible && shadowMetric <= 1+1e-12
}
phenomenon := JupiterGalileanPhenomenon{
Transit: onEarthDisk && observation.InFrontOfJupiter,
Occultation: onEarthDisk && !observation.InFrontOfJupiter,
Eclipse: eclipse,
ShadowTransit: shadowTransit,
ShadowOffsetXArcsec: math.NaN(),
ShadowOffsetYArcsec: math.NaN(),
ShadowOffsetXJupiterRadii: math.NaN(),
ShadowOffsetYJupiterRadii: math.NaN(),
}
if shadowTransit {
phenomenon.ShadowOffsetXArcsec = math.Atan2(shadowPoint.xAU, context.jupiterDistance) * deg * 3600
phenomenon.ShadowOffsetYArcsec = math.Atan2(shadowPoint.yAU, context.jupiterDistance) * deg * 3600
phenomenon.ShadowOffsetXJupiterRadii = shadowPoint.xJupiterRadii
phenomenon.ShadowOffsetYJupiterRadii = shadowPoint.yJupiterRadii
}
switch phenomenonType {
case JupiterGalileanTransit:
metric := earthMetric
if !observation.InFrontOfJupiter {
metric += 4
}
return jupiterGalileanMetricSample{active: phenomenon.Transit, metric: metric, phenomenon: phenomenon}
case JupiterGalileanOccultation:
metric := earthMetric
if observation.InFrontOfJupiter {
metric += 4
}
return jupiterGalileanMetricSample{active: phenomenon.Occultation, metric: metric, phenomenon: phenomenon}
case JupiterGalileanEclipse:
metric := sunMetric
if zSunAU <= 0 {
metric += 4
}
return jupiterGalileanMetricSample{active: phenomenon.Eclipse, metric: metric, phenomenon: phenomenon}
case JupiterGalileanShadowTransit:
metric := shadowMetric
if shadowPoint.hasIntersection && !shadowPoint.visible {
metric += 4
}
return jupiterGalileanMetricSample{active: phenomenon.ShadowTransit, metric: metric, phenomenon: phenomenon}
default:
return jupiterGalileanMetricSample{metric: math.Inf(1), phenomenon: invalidJupiterGalileanPhenomenon()}
}
}
func (context jupiterGalileanObservationContext) shadowPointFor(stateVector Vector3) jupiterGalileanShadowPoint {
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
satelliteBody := context.toBodyCoordinates(stateVector)
satelliteBody = Vector3{
satelliteBody[0] / radiusAU,
satelliteBody[1] / radiusAU,
satelliteBody[2] / radiusAU,
}
directionBody := context.toBodyCoordinates(context.sunLineOfSight)
intersectionBody, ok := ellipsoidRayIntersection(satelliteBody, directionBody, jupiterPolarRadiusRatio())
if !ok {
return jupiterGalileanShadowPoint{}
}
normalBody := Vector3{
intersectionBody[0],
intersectionBody[1],
intersectionBody[2] / (jupiterPolarRadiusRatio() * jupiterPolarRadiusRatio()),
}
earthBody := context.toBodyCoordinates(context.earthDirection)
intersection := context.fromBodyCoordinates(Vector3{
intersectionBody[0] * radiusAU,
intersectionBody[1] * radiusAU,
intersectionBody[2] * radiusAU,
})
dx := intersection[0] - stateVector[0]
dy := intersection[1] - stateVector[1]
dz := intersection[2] - stateVector[2]
xAU := vectorDot(intersection, context.east)
yAU := vectorDot(intersection, context.north)
xR := xAU / radiusAU
yR := yAU / radiusAU
return jupiterGalileanShadowPoint{
hasIntersection: true,
visible: vectorDot(normalBody, earthBody) > 0,
pathLengthAU: math.Sqrt(dx*dx + dy*dy + dz*dz),
xAU: xAU,
yAU: yAU,
xJupiterRadii: xR,
yJupiterRadii: yR,
}
}
func jupiterGalileanOrbitPeriodDays(satellite int) float64 {
switch satellite {
case 1:
return 1.769137786
case 2:
return 3.551181
case 3:
return 7.154553
case 4:
return 16.689018
default:
return math.NaN()
}
}
func jupiterGalileanCoarseStepDays(satellite int) float64 {
step := jupiterGalileanOrbitPeriodDays(satellite) / 16
maxStep := 2.0 / 24.0
if step > maxStep {
return maxStep
}
return step
}
func jupiterGalileanBoundaryStep(satellite int) float64 {
step := jupiterGalileanOrbitPeriodDays(satellite) / 32
if step > jupiterGalileanBoundaryStepDays {
return jupiterGalileanBoundaryStepDays
}
return step
}
func jupiterGalileanEventMatchesDirection(eventJD, targetJD float64, direction int, includeCurrent bool) bool {
diff := eventJD - targetJD
switch {
case direction < 0 && includeCurrent:
return diff <= jupiterGalileanEventEpsilonDays
case direction < 0:
return diff < -jupiterGalileanEventEpsilonDays
case includeCurrent:
return diff >= -jupiterGalileanEventEpsilonDays
default:
return diff > jupiterGalileanEventEpsilonDays
}
}
func ellipseMetric(x, y, major, minor float64) float64 {
if major <= 0 || minor <= 0 {
return math.Inf(1)
}
return (x*x)/(major*major) + (y*y)/(minor*minor)
}
func isValidJupiterGalileanPhenomenonType(phenomenonType JupiterGalileanPhenomenonType) bool {
switch phenomenonType {
case JupiterGalileanTransit, JupiterGalileanOccultation, JupiterGalileanEclipse, JupiterGalileanShadowTransit:
return true
default:
return false
}
}
func invalidJupiterGalileanPhenomenonEvent() JupiterGalileanPhenomenonEvent {
return JupiterGalileanPhenomenonEvent{
Start: math.NaN(),
Greatest: math.NaN(),
End: math.NaN(),
GreatestPhenomenon: invalidJupiterGalileanPhenomenon(),
}
}
+145
View File
@@ -0,0 +1,145 @@
package basic
import (
"encoding/json"
"math"
"os"
"path/filepath"
"testing"
"time"
)
const galileanEventToleranceSeconds = 480.0
type galileanEventBaselineRecord struct {
Label string `json:"label"`
Satellite int `json:"satellite"`
Type string `json:"type"`
StartUTC string `json:"start_utc"`
StartDurationMinutes float64 `json:"start_duration_minutes"`
EndUTC string `json:"end_utc"`
EndDurationMinutes float64 `json:"end_duration_minutes"`
}
func TestJupiterGalileanPhenomenonEventsAgainstIMCCEBaseline(t *testing.T) {
records := loadGalileanEventBaseline(t)
maxStartDiff := 0.0
maxEndDiff := 0.0
for _, record := range records {
startUTC := mustParseRFC3339Nano(t, record.StartUTC)
endUTC := mustParseRFC3339Nano(t, record.EndUTC)
queryBefore := startUTC.Add(-12 * time.Hour)
queryAfter := endUTC.Add(12 * time.Hour)
queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2)
phenomenonType := parseBasicGalileanPhenomenonType(t, record.Type)
next := NextJupiterGalileanPhenomenonEvent(Date2JDE(queryBefore.UTC()), record.Satellite, phenomenonType)
last := LastJupiterGalileanPhenomenonEvent(Date2JDE(queryAfter.UTC()), record.Satellite, phenomenonType)
closest := ClosestJupiterGalileanPhenomenonEvent(Date2JDE(queryMid.UTC()), record.Satellite, phenomenonType)
assertGalileanEventMatchesBaseline(t, record.Label+" next", next, record, startUTC, endUTC, &maxStartDiff, &maxEndDiff)
assertGalileanEventMatchesBaseline(t, record.Label+" last", last, record, startUTC, endUTC, &maxStartDiff, &maxEndDiff)
assertGalileanEventMatchesBaseline(t, record.Label+" closest", closest, record, startUTC, endUTC, &maxStartDiff, &maxEndDiff)
}
t.Logf("galilean event baseline max diff: start=%.1fs end=%.1fs", maxStartDiff, maxEndDiff)
}
func assertGalileanEventMatchesBaseline(
t *testing.T,
name string,
event JupiterGalileanPhenomenonEvent,
record galileanEventBaselineRecord,
startUTC, endUTC time.Time,
maxStartDiff, maxEndDiff *float64,
) {
t.Helper()
if !event.Valid {
t.Fatalf("%s invalid event", name)
}
if event.Satellite != record.Satellite {
t.Fatalf("%s satellite mismatch: got %d want %d", name, event.Satellite, record.Satellite)
}
if string(event.Type) != record.Type {
t.Fatalf("%s type mismatch: got %q want %q", name, event.Type, record.Type)
}
gotStart := JDE2DateByZone(event.Start, time.UTC, false)
gotEnd := JDE2DateByZone(event.End, time.UTC, false)
startDiff := math.Abs(gotStart.Sub(startUTC).Seconds())
endDiff := math.Abs(gotEnd.Sub(endUTC).Seconds())
if startDiff > *maxStartDiff {
*maxStartDiff = startDiff
}
if endDiff > *maxEndDiff {
*maxEndDiff = endDiff
}
if startDiff > galileanEventToleranceSeconds {
t.Fatalf("%s start mismatch: got %s want %s", name, gotStart.Format(time.RFC3339Nano), startUTC.Format(time.RFC3339Nano))
}
if endDiff > galileanEventToleranceSeconds {
t.Fatalf("%s end mismatch: got %s want %s", name, gotEnd.Format(time.RFC3339Nano), endUTC.Format(time.RFC3339Nano))
}
if !(event.Start <= event.Greatest && event.Greatest <= event.End) {
t.Fatalf("%s greatest not inside event: start=%.9f greatest=%.9f end=%.9f", name, event.Start, event.Greatest, event.End)
}
if !galileanPhenomenonFlag(event.GreatestPhenomenon, event.Type) {
t.Fatalf("%s greatest state is not active", name)
}
if jupiterGalileanPhenomenonMetricAt(event.Start-5.0/86400.0, event.Satellite, event.Type).active {
t.Fatalf("%s still active 5s before start", name)
}
if jupiterGalileanPhenomenonMetricAt(event.End+5.0/86400.0, event.Satellite, event.Type).active {
t.Fatalf("%s still active 5s after end", name)
}
}
func galileanPhenomenonFlag(phenomenon JupiterGalileanPhenomenon, phenomenonType JupiterGalileanPhenomenonType) bool {
switch phenomenonType {
case JupiterGalileanTransit:
return phenomenon.Transit
case JupiterGalileanOccultation:
return phenomenon.Occultation
case JupiterGalileanEclipse:
return phenomenon.Eclipse
case JupiterGalileanShadowTransit:
return phenomenon.ShadowTransit
default:
return false
}
}
func parseBasicGalileanPhenomenonType(t *testing.T, value string) JupiterGalileanPhenomenonType {
t.Helper()
switch JupiterGalileanPhenomenonType(value) {
case JupiterGalileanTransit, JupiterGalileanOccultation, JupiterGalileanEclipse, JupiterGalileanShadowTransit:
return JupiterGalileanPhenomenonType(value)
default:
t.Fatalf("unknown galilean phenomenon type %q", value)
return ""
}
}
func loadGalileanEventBaseline(t *testing.T) []galileanEventBaselineRecord {
t.Helper()
path := filepath.Join("..", "jupiter", "testdata", "galilean_events_imcce_2026.json")
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var records []galileanEventBaselineRecord
if err := json.Unmarshal(data, &records); err != nil {
t.Fatal(err)
}
if len(records) == 0 {
t.Fatal("empty galilean event baseline")
}
return records
}
func mustParseRFC3339Nano(t *testing.T, value string) time.Time {
t.Helper()
date, err := time.Parse(time.RFC3339Nano, value)
if err != nil {
t.Fatalf("parse %q: %v", value, err)
}
return date
}
+864
View File
@@ -0,0 +1,864 @@
package basic
// Code generated by scripts/jupiter-galilean-l1/main.go; DO NOT EDIT.
var jupiterGalileanL1LongPeriodTerms = [4][4][]jupiterGalileanL1Term{
{
{
{Amp: 0.0000000006827622, Period: 462.51460581, Phase: -2.709383024651},
{Amp: 0.0000000006827622, Period: -462.51460581, Phase: 2.709383056866},
{Amp: 0.0000000003280812, Period: -482.05605144, Phase: 1.755848247185},
{Amp: 0.0000000003280812, Period: 482.05605145, Phase: -1.755848223779},
{Amp: 0.0000000001706288, Period: -403.51538063, Phase: -2.610358561262},
{Amp: 0.0000000001706288, Period: 403.51538062, Phase: 2.610358537855},
{Amp: 0.0000000001855996, Period: 485.60320176, Phase: -0.112246363060},
{Amp: 0.0000000001855996, Period: -485.60320175, Phase: 0.112246363894},
{Amp: 0.0000000001507209, Period: -254.63444650, Phase: -0.161685950186},
{Amp: 0.0000000001507209, Period: 254.63444650, Phase: 0.161685950186},
{Amp: 0.0000000000724860, Period: 2059.62104200, Phase: 2.190529222727},
{Amp: 0.0000000000724860, Period: -2059.62104200, Phase: -2.190529222727},
{Amp: 0.0000000000272746, Period: -274.02279431, Phase: -1.229708703052},
{Amp: 0.0000000000272746, Period: 274.02279431, Phase: 1.229708703058},
{Amp: 0.0000000000163051, Period: 250.13067987, Phase: 2.545191166411},
{Amp: 0.0000000000163051, Period: -250.13067987, Phase: -2.545191131299},
},
{
{Amp: 0.0000962629174333, Period: -462.51460360, Phase: 1.138619193016},
{Amp: 0.0000962629174333, Period: 462.51460361, Phase: -1.138619147350},
{Amp: 0.0000485401798038, Period: 482.05605148, Phase: -0.185051689928},
{Amp: 0.0000485401798038, Period: -482.05605148, Phase: 0.185051701631},
{Amp: 0.0000449408708250, Period: -2059.62271753, Phase: 2.521072536783},
{Amp: 0.0000449408708250, Period: 2059.62271753, Phase: -2.521072536783},
{Amp: 0.0000276550525131, Period: -485.60320179, Phase: -1.458550472778},
{Amp: 0.0000276550525131, Period: 485.60320179, Phase: 1.458550472730},
{Amp: 0.0000209039435245, Period: -403.51538158, Phase: 2.102023003408},
{Amp: 0.0000209039435245, Period: 403.51538158, Phase: -2.102022944892},
{Amp: 0.0000186178298694, Period: 4332.93914977, Phase: 2.108666755900},
{Amp: 0.0000186178298694, Period: -4332.93914965, Phase: -2.108666744160},
{Amp: 0.0000117220266508, Period: 254.63444653, Phase: 1.732483394545},
{Amp: 0.0000117220266508, Period: -254.63444653, Phase: -1.732483394547},
{Amp: 0.0000080156582120, Period: -66002.48715215, Phase: 0.073578008624},
{Amp: 0.0000080156582120, Period: 66002.48716936, Phase: -0.073578000960},
{Amp: 0.0000041114727746, Period: -11428.11256828, Phase: 2.016207414938},
{Amp: 0.0000041114727746, Period: 11428.11256829, Phase: -2.016207414823},
{Amp: 0.0000043608559052, Period: 14133.23625267, Phase: 0.738064832500},
{Amp: 0.0000043608559052, Period: -14133.23624426, Phase: -0.738064751637},
{Amp: 0.0000037682740860, Period: 9692.27262460, Phase: 1.002642850091},
{Amp: 0.0000037682740860, Period: -9692.27262347, Phase: -1.002642825467},
{Amp: 0.0000026598151336, Period: 2166.51917628, Phase: -0.118101155294},
{Amp: 0.0000026598151336, Period: -2166.51917619, Phase: 0.118101190565},
},
{
{Amp: 0.0041510849668155, Period: -486.80959494, Phase: 2.009725283179},
{Amp: 0.0000352747346169, Period: 49367.32662218, Phase: 0.253909334368},
{Amp: 0.0000198194483636, Period: 195946.92419811, Phase: 1.897179483608},
{Amp: 0.0000146399842989, Period: 2358.32875876, Phase: 1.478395862207},
{Amp: 0.0000052220588690, Period: 9267.76794016, Phase: 2.447815000610},
{Amp: 0.0000022044764663, Period: -237.17561538, Phase: -1.564292084282},
{Amp: 0.0000015410375360, Period: 2265.74291645, Phase: -1.823335732733},
{Amp: 0.0000012193607962, Period: -637.45860369, Phase: -1.957542685623},
},
{
{Amp: 0.0003142172466014, Period: -2714.00626203, Phase: 0.017009891895},
{Amp: 0.0000904169207946, Period: -11038.50115941, Phase: 2.090564874290},
{Amp: 0.0000164452324013, Period: -50300.46261866, Phase: -1.808667780761},
{Amp: 0.0000055424829091, Period: -205593.77774416, Phase: 2.907426236797},
{Amp: 0.0000035856270353, Period: -248.89302051, Phase: 1.928881950702},
{Amp: 0.0000024180760140, Period: 2166.34061401, Phase: 2.301952132272},
{Amp: 0.0000008673084930, Period: -4333.05393383, Phase: 1.875397350363},
{Amp: 0.0000003176227277, Period: -267.38514937, Phase: 0.860886171255},
{Amp: 0.0000003152816608, Period: 1444.25588657, Phase: 2.837887905087},
{Amp: 0.0000002338676726, Period: 4332.83109038, Phase: -2.991881157330},
{Amp: 0.0000001754553689, Period: -244.58835226, Phase: -0.454720421567},
{Amp: 0.0000001286319583, Period: -243.40543219, Phase: 1.572977535435},
{Amp: 0.0000000967213304, Period: -2166.50569931, Phase: -1.943113737418},
},
},
{
{
{Amp: 0.0000000090655385, Period: 482.05605605, Phase: 1.385772645301},
{Amp: 0.0000000090655385, Period: -482.05605605, Phase: -1.385772633599},
{Amp: 0.0000000047683565, Period: -485.60320520, Phase: -3.029413042035},
{Amp: 0.0000000047683565, Period: 485.60320520, Phase: 3.029413042096},
{Amp: 0.0000000035969671, Period: 462.51472501, Phase: -2.707591871848},
{Amp: 0.0000000035969671, Period: -462.51472501, Phase: 2.707591896577},
{Amp: 0.0000000008232399, Period: -403.51538403, Phase: 0.531187191370},
{Amp: 0.0000000008232399, Period: 403.51538403, Phase: -0.531187167963},
{Amp: 0.0000000005125913, Period: 2059.62196661, Phase: -0.950637805889},
{Amp: 0.0000000005125913, Period: -2059.62196661, Phase: 0.950637805889},
{Amp: 0.0000000003478592, Period: 250.13068971, Phase: -0.596195111066},
{Amp: 0.0000000003478592, Period: -250.13068971, Phase: 0.596195134473},
{Amp: 0.0000000002908957, Period: -254.63444649, Phase: 2.979903215092},
{Amp: 0.0000000002908957, Period: 254.63444649, Phase: -2.979903215092},
{Amp: 0.0000000001698689, Period: -241.91156453, Phase: 1.867830046014},
{Amp: 0.0000000001698689, Period: 241.91156453, Phase: -1.867830013805},
{Amp: 0.0000000001896089, Period: -248.89380995, Phase: 2.620073542296},
{Amp: 0.0000000001896089, Period: 248.89380995, Phase: -2.620073542267},
{Amp: 0.0000000001269077, Period: -274.02280385, Phase: 1.911888048968},
{Amp: 0.0000000001269077, Period: 274.02280385, Phase: -1.911888048527},
{Amp: 0.0000000001579746, Period: -241.02802357, Phase: -2.771389990456},
{Amp: 0.0000000001579746, Period: 241.02802357, Phase: 2.771389988517},
},
{
{Amp: 0.0004288216586468, Period: 482.05605608, Phase: 2.956569304014},
{Amp: 0.0004288216586468, Period: -482.05605608, Phase: -2.956569292310},
{Amp: 0.0002274791437543, Period: 485.60320524, Phase: -1.682976093416},
{Amp: 0.0002274791437547, Period: -485.60320524, Phase: 1.682976093476},
{Amp: 0.0001537125039667, Period: -462.51473011, Phase: 1.136719181022},
{Amp: 0.0001537125039667, Period: 462.51473011, Phase: -1.136719137618},
{Amp: 0.0000917031775902, Period: -4332.93871803, Phase: -2.108663081132},
{Amp: 0.0000917031775902, Period: 4332.93871814, Phase: 2.108663092871},
{Amp: 0.0000991193072392, Period: 2059.37736140, Phase: 0.507502870024},
{Amp: 0.0000991193072392, Period: -2059.37736140, Phase: -0.507502870018},
{Amp: 0.0000316144888598, Period: -403.51538512, Phase: -1.039617955783},
{Amp: 0.0000316144888598, Period: 403.51538512, Phase: 1.039617990892},
{Amp: 0.0000174586375148, Period: -210240.18087863, Phase: 1.750407353612},
{Amp: 0.0000174586375134, Period: 210240.18167626, Phase: -1.750407317793},
{Amp: 0.0000162854547323, Period: 50284.65590194, Phase: -0.490087477075},
{Amp: 0.0000162854547324, Period: -50284.65590078, Phase: 0.490087477963},
{Amp: 0.0000102913236930, Period: -2166.51734380, Phase: -3.053369407734},
{Amp: 0.0000102913236930, Period: 2166.51734389, Phase: 3.053369442987},
{Amp: 0.0000084014158127, Period: 250.13068976, Phase: 0.974602969007},
{Amp: 0.0000084014158127, Period: -250.13068976, Phase: -0.974602933897},
{Amp: 0.0000070356577800, Period: 254.63444650, Phase: -1.409106426418},
{Amp: 0.0000070356577800, Period: -254.63444650, Phase: 1.409106424511},
{Amp: 0.0000070814366803, Period: -9676.80000028, Phase: 2.607323588975},
{Amp: 0.0000070814366803, Period: 9676.80000028, Phase: -2.607323588975},
{Amp: 0.0000050373040117, Period: 11362.43599928, Phase: 0.577394871339},
{Amp: 0.0000050373040117, Period: -11362.43599928, Phase: -0.577394871340},
{Amp: 0.0000053299308810, Period: 2078.23892619, Phase: -0.021997688216},
{Amp: 0.0000053299308811, Period: -2078.23892619, Phase: 0.021997688222},
{Amp: 0.0000047325683320, Period: 67215.42643142, Phase: 0.141213175007},
{Amp: 0.0000047325683325, Period: -67215.42643262, Phase: -0.141213175548},
{Amp: 0.0000039214851670, Period: 241.91154490, Phase: -0.297536953260},
{Amp: 0.0000039214851670, Period: -241.91154490, Phase: 0.297536962655},
{Amp: 0.0000045554036662, Period: 248.89381141, Phase: -1.049231904996},
{Amp: 0.0000045554036661, Period: -248.89381141, Phase: 1.049231905683},
},
{
{Amp: 0.0093589104136341, Period: -486.80959494, Phase: -1.131867355050},
{Amp: 0.0001980963564781, Period: 9267.59815105, Phase: -0.700195659441},
{Amp: 0.0002139036390350, Period: 49367.31948358, Phase: 0.253903838790},
{Amp: 0.0001210388158965, Period: 196002.76969709, Phase: 1.900452411216},
{Amp: 0.0000139052321679, Period: -242.21058266, Phase: -2.517653962057},
{Amp: 0.0000073925894084, Period: -243.10282624, Phase: 2.121892237182},
{Amp: 0.0000051968296512, Period: -237.17564686, Phase: 1.575472602669},
},
{
{Amp: 0.0040404917832303, Period: -11038.50070470, Phase: 2.090572293375},
{Amp: 0.0002200421034564, Period: -50300.46217493, Phase: -1.808667829190},
{Amp: 0.0000590282470983, Period: -205590.83436962, Phase: 2.907535129258},
{Amp: 0.0000105030331400, Period: -2714.00565902, Phase: -3.124346970559},
{Amp: 0.0000102943248250, Period: -248.89302050, Phase: -1.212709792839},
{Amp: 0.0000072600013020, Period: 2166.34096750, Phase: 2.302095210918},
{Amp: 0.0000018391258758, Period: -4333.05816401, Phase: -1.266797143358},
{Amp: 0.0000014880605763, Period: -244.58836553, Phase: -0.455088252478},
{Amp: 0.0000008828196274, Period: 1810.91607041, Phase: -0.522805812968},
{Amp: 0.0000008714042768, Period: 4332.82720044, Phase: -2.991672113265},
{Amp: 0.0000008536188044, Period: 1444.25631462, Phase: 2.838189555021},
{Amp: 0.0000006846214331, Period: -243.40441644, Phase: 1.606346686085},
{Amp: 0.0000004471826348, Period: -267.38514860, Phase: -2.280679314445},
{Amp: 0.0000003034392168, Period: -243.69327303, Phase: 1.113159867990},
{Amp: 0.0000001792048645, Period: -3111.52953257, Phase: 1.563078661742},
{Amp: 0.0000001799083735, Period: 7133.00782851, Phase: -0.505644195617},
{Amp: 0.0000001062153531, Period: -11679.84994953, Phase: 1.447094472227},
{Amp: 0.0000001098546626, Period: 1277.13984095, Phase: 0.006562244228},
{Amp: 0.0000001083128732, Period: -10479.05596460, Phase: -0.636294555123},
{Amp: 0.0000000768496749, Period: 1083.20345403, Phase: -2.906284048843},
{Amp: 0.0000000692273841, Period: 2076.86504405, Phase: -2.906237638896},
{Amp: 0.0000000676969224, Period: -14141.63136873, Phase: 0.044806456994},
{Amp: 0.0000000621559952, Period: -461.88727875, Phase: 0.684245423843},
},
},
{
{
{Amp: 0.0000000054767566, Period: 462.51465961, Phase: 0.433012270674},
{Amp: 0.0000000054767566, Period: -462.51465961, Phase: -0.433012249211},
{Amp: 0.0000000056033730, Period: -482.05605692, Phase: 1.755815363396},
{Amp: 0.0000000056033730, Period: 482.05605692, Phase: -1.755815351694},
{Amp: 0.0000000028921342, Period: 485.60320600, Phase: -0.112163580985},
{Amp: 0.0000000028921342, Period: -485.60320600, Phase: 0.112163580799},
{Amp: 0.0000000003409630, Period: 254.63444667, Phase: -2.979904123822},
{Amp: 0.0000000003409630, Period: -254.63444667, Phase: 2.979904123823},
{Amp: 0.0000000002216888, Period: 250.13068507, Phase: 2.545311746332},
{Amp: 0.0000000002216888, Period: -250.13068507, Phase: -2.545311746332},
{Amp: 0.0000000001418329, Period: -241.91155085, Phase: -1.273238319314},
{Amp: 0.0000000001418329, Period: 241.91155085, Phase: 1.273238319313},
},
{
{Amp: 0.0001155398943113, Period: 4332.93886608, Phase: 2.108691866697},
{Amp: 0.0001155398943113, Period: -4332.93886574, Phase: -2.108691831480},
{Amp: 0.0000914317982059, Period: -482.05605702, Phase: 0.185017491519},
{Amp: 0.0000914317982059, Period: 482.05605703, Phase: -0.185017456411},
{Amp: 0.0000756189389102, Period: 462.51466388, Phase: 2.003875199838},
{Amp: 0.0000756189389102, Period: -462.51466387, Phase: -2.003875175333},
{Amp: 0.0000477739034923, Period: 485.60320603, Phase: 1.458631213994},
{Amp: 0.0000477739034926, Period: -485.60320603, Phase: -1.458631213811},
{Amp: 0.0000271961236501, Period: 210274.48426729, Phase: -1.785123499277},
{Amp: 0.0000271961236540, Period: -210274.48346940, Phase: 1.785123534351},
{Amp: 0.0000244626823237, Period: -50284.47674577, Phase: 0.491194059142},
{Amp: 0.0000244626823247, Period: 50284.47674762, Phase: -0.491194057704},
{Amp: 0.0000077362945254, Period: 9642.10026223, Phase: 2.580679451841},
{Amp: 0.0000077362945254, Period: -9642.10026227, Phase: -2.580679452618},
{Amp: 0.0000084883673229, Period: 2059.54227670, Phase: -2.558255736482},
{Amp: 0.0000084883673229, Period: -2059.54227670, Phase: 2.558255736428},
{Amp: 0.0000067827229369, Period: -11287.76456818, Phase: -0.945782523799},
{Amp: 0.0000067827229369, Period: 11287.76456818, Phase: 0.945782523799},
{Amp: 0.0000037731001815, Period: -67979.99326489, Phase: -0.181520301079},
{Amp: 0.0000037731001804, Period: 67979.99326458, Phase: 0.181520300800},
{Amp: 0.0000032076570609, Period: 2166.51777632, Phase: 3.080339678337},
{Amp: 0.0000032076570609, Period: -2166.51777629, Phase: -3.080339666591},
{Amp: 0.0000026285122860, Period: -254.63444659, Phase: 1.409111585164},
{Amp: 0.0000026285122860, Period: 254.63444659, Phase: -1.409111565386},
{Amp: 0.0000028282486512, Period: 14014.13678272, Phase: 2.891690797470},
{Amp: 0.0000028282486512, Period: -14014.13678272, Phase: -2.891690797471},
},
{
{Amp: 0.0014289811307319, Period: 49367.34034574, Phase: 0.253921225395},
{Amp: 0.0007710931226760, Period: 195955.54100807, Phase: 1.897688885982},
{Amp: 0.0005925911780766, Period: -486.80959494, Phase: 2.009725311237},
{Amp: 0.0000070808673396, Period: -242.21058256, Phase: 0.623939292029},
{Amp: 0.0000070804404020, Period: 9270.75474882, Phase: 2.567494996183},
{Amp: 0.0000048299146941, Period: 2265.73531869, Phase: 1.315002005009},
{Amp: 0.0000038142769361, Period: -243.10282598, Phase: -1.019683047586},
{Amp: 0.0000022651772374, Period: 4332.66561900, Phase: 0.788371189690},
{Amp: 0.0000023143850535, Period: 2190.53482611, Phase: -0.326448440973},
{Amp: 0.0000007268381420, Period: -237.17572590, Phase: -1.570723011296},
},
{
{Amp: 0.0015932721570848, Period: -50300.46317781, Phase: -1.808668110228},
{Amp: 0.0003513347911037, Period: -205594.76675742, Phase: 2.907439275438},
{Amp: 0.0001441929255483, Period: -11038.50233695, Phase: -1.051046669426},
{Amp: 0.0000157303527750, Period: 2166.34184154, Phase: 2.302490124225},
{Amp: 0.0000025161319881, Period: -4333.06554786, Phase: -1.267594020600},
{Amp: 0.0000020438305183, Period: 4332.81770831, Phase: -2.991340015519},
{Amp: 0.0000017939612784, Period: 1444.25697259, Phase: 2.838852837237},
{Amp: 0.0000013614276895, Period: -248.89302051, Phase: 1.928883308456},
{Amp: 0.0000008996109017, Period: 2076.86534892, Phase: -2.906045098558},
{Amp: 0.0000008702078430, Period: -2714.00571514, Phase: -3.123659682744},
{Amp: 0.0000004371144064, Period: -244.58835722, Phase: 2.686731305626},
{Amp: 0.0000001926397869, Period: 2142.19483368, Phase: -1.985925544377},
{Amp: 0.0000002174259374, Period: -243.40560179, Phase: -1.574163434803},
{Amp: 0.0000001589279656, Period: 1083.20364160, Phase: -2.905875929922},
{Amp: 0.0000001432228753, Period: -3989.29234760, Phase: -2.346001284423},
},
},
{
{
{Amp: 0.0000000001238821, Period: 9641.95997751, Phase: -2.136521368996},
{Amp: 0.0000000001238821, Period: -9641.95997747, Phase: 2.136521369695},
{Amp: 0.0000000000937288, Period: 11292.66166947, Phase: 2.658438162555},
{Amp: 0.0000000000937288, Period: -11292.66166870, Phase: -2.658438150854},
},
{
{Amp: 0.0002793020061912, Period: -4332.93860171, Phase: -2.108786357946},
{Amp: 0.0002793020061912, Period: 4332.93860182, Phase: 2.108786369685},
{Amp: 0.0001902906934088, Period: 211344.06814397, Phase: 1.346615691710},
{Amp: 0.0001902906934113, Period: -211344.06760662, Phase: -1.346615668368},
{Amp: 0.0000167518242157, Period: -50289.57428101, Phase: -2.654427789777},
{Amp: 0.0000167518242264, Period: 50289.57429022, Phase: 2.654427797106},
{Amp: 0.0000092970467736, Period: -2166.51555964, Phase: -2.989880275328},
{Amp: 0.0000092970467736, Period: 2166.51555970, Phase: 2.989880298849},
{Amp: 0.0000085219011443, Period: 9641.95655559, Phase: -0.565865906865},
{Amp: 0.0000085219011444, Period: -9641.95655560, Phase: 0.565865906783},
{Amp: 0.0000075696916557, Period: -11291.33721849, Phase: 2.059123798134},
{Amp: 0.0000075696916557, Period: 11291.33721925, Phase: -2.059123786437},
{Amp: 0.0000058058699386, Period: -67440.56838013, Phase: 2.926756672932},
{Amp: 0.0000058058699344, Period: 67440.56837337, Phase: -2.926756675958},
},
{
{Amp: 0.0073755808467977, Period: 195950.90846202, Phase: 1.897427359615},
{Amp: 0.0001561131605348, Period: 49367.23074528, Phase: -2.887649087328},
{Amp: 0.0000540660842731, Period: 2190.52954139, Phase: -0.328140763057},
{Amp: 0.0000090411191759, Period: 4332.68219367, Phase: 0.789967351008},
{Amp: 0.0000060406575087, Period: 1454.71609134, Phase: -0.020851153251},
{Amp: 0.0000028551622596, Period: -198930.12372162, Phase: -0.327774752201},
{Amp: 0.0000026588026505, Period: 4430.38639549, Phase: 2.290679594163},
{Amp: 0.0000016317841395, Period: 4238.97138743, Phase: 2.407098908451},
{Amp: 0.0000016159095087, Period: -4430.93037398, Phase: -1.783540964911},
{Amp: 0.0000012029942283, Period: 2265.73177106, Phase: -1.827994616498},
{Amp: 0.0000008869382117, Period: 9192.14352819, Phase: -0.159251424576},
{Amp: 0.0000004518928658, Period: 2164.87438400, Phase: 0.627199957599},
{Amp: 0.0000004739086661, Period: 10677.43163337, Phase: -1.721255818131},
{Amp: 0.0000005321318002, Period: 1089.21699649, Phase: 0.749679902895},
},
{
{Amp: 0.0022453891791894, Period: -205593.16871630, Phase: 2.907607823156},
{Amp: 0.0002604479450559, Period: -50300.45144562, Phase: 1.332921952672},
{Amp: 0.0000332112143230, Period: 2166.33408559, Phase: 2.299271304157},
{Amp: 0.0000049727136261, Period: -4333.06038571, Phase: -1.266951160328},
{Amp: 0.0000049416729114, Period: -11038.56696703, Phase: -1.052098159774},
{Amp: 0.0000043945193428, Period: 4332.82948108, Phase: -2.986950453661},
{Amp: 0.0000037630501589, Period: 1444.25618193, Phase: 2.837982671867},
{Amp: 0.0000030823418750, Period: 2143.47705615, Phase: -1.442836410158},
{Amp: 0.0000004637177865, Period: -4242.89245749, Phase: 2.432125315770},
{Amp: 0.0000004719790711, Period: 4426.19806280, Phase: 0.277692210330},
{Amp: 0.0000003467132626, Period: 1433.98920963, Phase: -0.966205311247},
{Amp: 0.0000003497224175, Period: 2076.87114473, Phase: 0.238492877349},
{Amp: 0.0000003324412570, Period: 1083.20735284, Phase: -2.900230549782},
{Amp: 0.0000001945374351, Period: 2181.03566283, Phase: 2.659258242659},
{Amp: 0.0000001727743329, Period: -2166.53579568, Phase: -1.915921390647},
{Amp: 0.0000001485176585, Period: 4243.19386437, Phase: 1.378730646509},
},
},
}
var jupiterGalileanL1CrossPeriodTerms = [4][4][]jupiterGalileanL1Term{
{
{
{Amp: 0.0028210960212903, Period: 0.00000000, Phase: 0.000000000000},
{Amp: 0.0000000381012294, Period: 1.76273174, Phase: -2.643895074882},
{Amp: 0.0000000381012294, Period: -1.76273174, Phase: 2.643895075707},
{Amp: 0.0000000086168826, Period: 3.52546349, Phase: 1.819648782093},
{Amp: 0.0000000086168826, Period: -3.52546349, Phase: -1.819648774502},
{Amp: 0.0000000090450162, Period: -0.88136587, Phase: -0.995547070485},
{Amp: 0.0000000090450162, Period: 0.88136587, Phase: 0.995547070485},
{Amp: 0.0000000047397043, Period: -0.78343633, Phase: 2.807162813908},
{Amp: 0.0000000047397043, Period: 0.78343633, Phase: -2.807162778199},
{Amp: 0.0000000046098133, Period: -0.58757725, Phase: 1.648484811816},
{Amp: 0.0000000046098133, Period: 0.58757725, Phase: -1.648484776721},
{Amp: 0.0000000050863040, Period: 0.70509270, Phase: 2.815055976377},
{Amp: 0.0000000050863040, Period: -0.70509270, Phase: -2.815055941282},
{Amp: 0.0000000029290802, Period: -2.35030899, Phase: -1.158674633569},
{Amp: 0.0000000029290802, Period: 2.35030899, Phase: 1.158674656974},
{Amp: 0.0000000017446377, Period: 0.50363764, Phase: 0.171224706165},
{Amp: 0.0000000017446377, Period: -0.50363764, Phase: -0.171224671071},
{Amp: 0.0000000015421426, Period: 0.98945742, Phase: -2.666154170053},
{Amp: 0.0000000015421426, Period: -0.98945742, Phase: 2.666154193457},
{Amp: 0.0000000018109074, Period: -1.17515450, Phase: 0.824225124590},
{Amp: 0.0000000018109074, Period: 1.17515450, Phase: -0.824225124846},
{Amp: 0.0000000010397325, Period: -0.52701992, Phase: 1.990665563264},
{Amp: 0.0000000010397325, Period: 0.52701992, Phase: -1.990665563264},
{Amp: 0.0000000005841286, Period: 0.49036751, Phase: 0.489778750861},
{Amp: 0.0000000005841286, Period: -0.49036751, Phase: -0.489778750861},
{Amp: 0.0000000004015988, Period: 0.61965131, Phase: 2.472795335277},
{Amp: 0.0000000004015988, Period: -0.61965131, Phase: -2.472795323587},
{Amp: 0.0000000003507059, Period: -0.88493019, Phase: 1.953447599078},
{Amp: 0.0000000003507059, Period: 0.88493019, Phase: -1.953447575672},
{Amp: 0.0000000003654755, Period: 0.75178915, Phase: 0.653129619279},
{Amp: 0.0000000003654755, Period: -0.75178915, Phase: -0.653129619279},
{Amp: 0.0000000002876544, Period: 0.65963828, Phase: -0.857967356136},
{Amp: 0.0000000002876544, Period: -0.65963828, Phase: 0.857967374635},
{Amp: 0.0000000002179774, Period: 0.95555745, Phase: -1.167011088750},
{Amp: 0.0000000002179774, Period: -0.95555745, Phase: 1.167011088750},
{Amp: 0.0000000001614172, Period: 1.31085840, Phase: -2.986185416058},
{Amp: 0.0000000001614172, Period: -1.31085840, Phase: 2.986185416058},
{Amp: 0.0000000001716490, Period: 1.97891465, Phase: 1.799472338743},
{Amp: 0.0000000001716490, Period: -1.97891465, Phase: -1.799472336412},
{Amp: 0.0000000000827916, Period: 1.41018540, Phase: 2.978320583302},
{Amp: 0.0000000000827916, Period: -1.41018540, Phase: -2.978320559896},
{Amp: 0.0000000000763996, Period: 0.84151364, Phase: -1.827565110733},
{Amp: 0.0000000000763996, Period: -0.84151364, Phase: 1.827565116962},
{Amp: 0.0000000000806455, Period: 2.08677541, Phase: 1.477346039152},
{Amp: 0.0000000000806455, Period: -2.08677541, Phase: -1.477346042522},
{Amp: 0.0000000000853835, Period: 1.76920091, Phase: -1.211307445204},
{Amp: 0.0000000000853835, Period: -1.76920091, Phase: 1.211307445226},
{Amp: 0.0000000000761656, Period: 1.40607178, Phase: -1.650855579663},
{Amp: 0.0000000000761656, Period: -1.40607178, Phase: 1.650855579663},
{Amp: 0.0000000000594344, Period: -0.88511089, Phase: 0.951017241368},
{Amp: 0.0000000000594344, Period: 0.88511089, Phase: -0.951017239898},
{Amp: 0.0000000000564629, Period: 0.49472871, Phase: 0.950314977999},
{Amp: 0.0000000000564629, Period: -0.49472871, Phase: -0.950314966307},
{Amp: 0.0000000000493043, Period: -5.11358278, Phase: 0.341909441869},
{Amp: 0.0000000000493043, Period: 5.11358278, Phase: -0.341909430740},
{Amp: 0.0000000000428597, Period: 1.05181306, Phase: -2.914901885532},
{Amp: 0.0000000000428597, Period: -1.05181306, Phase: 2.914901883107},
{Amp: 0.0000000000438860, Period: 3.49879406, Phase: 0.481234140725},
{Amp: 0.0000000000438860, Period: -3.49879406, Phase: -0.481234137799},
},
{
{Amp: 1.4462132960212235, Period: 0.00000000, Phase: 0.000000000000},
{Amp: 0.0000251792213075, Period: 1.76273177, Phase: -1.071369371364},
{Amp: 0.0000251792213075, Period: -1.76273177, Phase: 1.071369371632},
{Amp: 0.0000222206385058, Period: -3.52546349, Phase: 2.892741097493},
{Amp: 0.0000222206385058, Period: 3.52546349, Phase: -2.892741074803},
{Amp: 0.0000054507134660, Period: 2.35030899, Phase: 2.729470598014},
{Amp: 0.0000054507134660, Period: -2.35030899, Phase: -2.729470598014},
{Amp: 0.0000059524877849, Period: 0.88136587, Phase: 2.566011851738},
{Amp: 0.0000059524877849, Period: -0.88136587, Phase: -2.566011851738},
{Amp: 0.0000028787912389, Period: -0.78343633, Phase: 1.236365304804},
{Amp: 0.0000028787912389, Period: 0.78343633, Phase: -1.236365280939},
{Amp: 0.0000030726401981, Period: -0.70509270, Phase: 1.897339035434},
{Amp: 0.0000030726401981, Period: 0.70509270, Phase: -1.897339023744},
{Amp: 0.0000023908706663, Period: -2056.36375976, Phase: -0.211262261618},
{Amp: 0.0000023908713552, Period: 2056.36431648, Phase: 0.211277724557},
{Amp: 0.0000022777007661, Period: 274.03235254, Phase: 0.387713436622},
{Amp: 0.0000022777007661, Period: -274.03235254, Phase: -0.387713436591},
{Amp: 0.0000025590603468, Period: -0.58757725, Phase: 0.077681276181},
{Amp: 0.0000025590603468, Period: 0.58757725, Phase: -0.077681252776},
{Amp: 0.0000021602067349, Period: 400.78715490, Phase: 0.013492534637},
{Amp: 0.0000021602067349, Period: -400.78715480, Phase: -0.013492473587},
{Amp: 0.0000018842049141, Period: 1.17515450, Phase: 0.746568951200},
{Amp: 0.0000018842049141, Period: -1.17515450, Phase: -0.746568951200},
{Amp: 0.0000015701869124, Period: -249.77392671, Phase: 2.493981352238},
{Amp: 0.0000015701869123, Period: 249.77392671, Phase: -2.493981352263},
{Amp: 0.0000008114937768, Period: -10400.18449376, Phase: -1.567931706749},
{Amp: 0.0000008114933753, Period: 10400.18102399, Phase: 1.567928771616},
{Amp: 0.0000012168267714, Period: 2471.07130547, Phase: -0.717589824480},
{Amp: 0.0000012168291116, Period: -2471.05875738, Phase: 0.717795029776},
{Amp: 0.0000010144950846, Period: -0.98945742, Phase: 1.095489123829},
{Amp: 0.0000010144950846, Period: 0.98945742, Phase: -1.095489112139},
{Amp: 0.0000009276215519, Period: 0.50363764, Phase: 1.741664442016},
{Amp: 0.0000009276215519, Period: -0.50363764, Phase: -1.741664395199},
{Amp: 0.0000009332719352, Period: -462.34993510, Phase: 3.008175468973},
{Amp: 0.0000009332719348, Period: 462.34993512, Phase: -3.008175463874},
{Amp: 0.0000001301198829, Period: 1375.10423851, Phase: -0.137037396295},
{Amp: 0.0000001301198573, Period: -1375.10422893, Phase: 0.137037333790},
{Amp: 0.0000004035364904, Period: 1668.45604889, Phase: -1.183442165366},
{Amp: 0.0000004035364083, Period: -1668.45590610, Phase: 1.183447162176},
{Amp: 0.0000006580493802, Period: -437.59465164, Phase: -3.042608902290},
{Amp: 0.0000006580493801, Period: 437.59465164, Phase: 3.042608902840},
},
{
{Amp: 0.0006260521444113, Period: 1.76913777, Phase: 1.446188898563},
{Amp: 0.0000096819265753, Period: 3.55118107, Phase: 2.768134001457},
{Amp: 0.0000098749504021, Period: -1.75637194, Phase: 0.450761187889},
{Amp: 0.0000083063168209, Period: 7.15455323, Phase: -2.854077904786},
{Amp: 0.0000045951340101, Period: -1.40611218, Phase: -2.029833930094},
{Amp: 0.0000059689735869, Period: 0.88296443, Phase: -1.209110059349},
{Amp: 0.0000046538995236, Period: -1.17232451, Phase: -1.368864973222},
{Amp: 0.0000037405126681, Period: -0.87977305, Phase: 3.094673734781},
{Amp: 0.0000037711061757, Period: -3.50011573, Phase: 2.270416702740},
{Amp: 0.0000018698303790, Period: -2.24513352, Phase: -2.170781015720},
{Amp: 0.0000013214613496, Period: -0.70407292, Phase: 1.275017772430},
{Amp: 0.0000012707585609, Period: 16.68653361, Phase: 1.972514861645},
{Amp: 0.0000011886104747, Period: 1.17799818, Phase: 0.124242374879},
{Amp: 0.0000007689215742, Period: -2302.10434716, Phase: 0.689291076961},
{Amp: 0.0000008742035177, Period: 2493.83004733, Phase: 2.390352831189},
},
{
{Amp: 0.0000175695395780, Period: 0.00000000, Phase: 2.415080967945},
{Amp: 0.0000000000692310, Period: -193.28859060, Phase: -2.208588622047},
},
},
{
{
{Amp: 0.0044871037804314, Period: 0.00000000, Phase: 0.000000000000},
{Amp: 0.0000002162183749, Period: 3.52546349, Phase: 1.819645606290},
{Amp: 0.0000002162183749, Period: -3.52546349, Phase: -1.819645606290},
{Amp: 0.0000000801807375, Period: -2.35030899, Phase: 1.982912654261},
{Amp: 0.0000000801807375, Period: 2.35030899, Phase: -1.982912619140},
{Amp: 0.0000000509573393, Period: -1.17515450, Phase: -2.317355432932},
{Amp: 0.0000000509573393, Period: 1.17515450, Phase: 2.317355432932},
{Amp: 0.0000000462367393, Period: -7.05092698, Phase: 0.660971402306},
{Amp: 0.0000000462367393, Period: 7.05092698, Phase: -0.660971402162},
{Amp: 0.0000000255754500, Period: -1.41018540, Phase: -2.978330737054},
{Amp: 0.0000000255754500, Period: 1.41018540, Phase: 2.978330748744},
{Amp: 0.0000000261832900, Period: 1.76273174, Phase: 0.497691978790},
{Amp: 0.0000000261832900, Period: -1.76273174, Phase: -0.497691978790},
{Amp: 0.0000000136429969, Period: -0.70509270, Phase: 0.327144622755},
{Amp: 0.0000000136429969, Period: 0.70509270, Phase: -0.327144587998},
{Amp: 0.0000000155953890, Period: -0.88136587, Phase: 2.146927076004},
{Amp: 0.0000000155953890, Period: 0.88136587, Phase: -2.146927075722},
{Amp: 0.0000000087480272, Period: 1.00727528, Phase: 1.656394163933},
{Amp: 0.0000000087480272, Period: -1.00727528, Phase: -1.656394128838},
{Amp: 0.0000000116112914, Period: 2.25553591, Phase: -0.022341870823},
{Amp: 0.0000000116112914, Period: -2.25553591, Phase: 0.022341870823},
{Amp: 0.0000000061437036, Period: 0.58757726, Phase: 1.500536383468},
{Amp: 0.0000000061437036, Period: -0.58757726, Phase: -1.500536348490},
{Amp: 0.0000000042431918, Period: -0.50363764, Phase: 2.970124107071},
{Amp: 0.0000000042431918, Period: 0.50363764, Phase: -2.970124071981},
{Amp: 0.0000000034561177, Period: -1.50369059, Phase: 0.034310693267},
{Amp: 0.0000000034561177, Period: 1.50369059, Phase: -0.034310721884},
{Amp: 0.0000000030688784, Period: 0.78343633, Phase: 0.334349762913},
{Amp: 0.0000000030688784, Period: -0.78343633, Phase: -0.334349729096},
{Amp: 0.0000000022671527, Period: 0.52701992, Phase: 1.152376957614},
{Amp: 0.0000000022671527, Period: -0.52701992, Phase: -1.152376945916},
{Amp: 0.0000000022287342, Period: 1.77703490, Phase: 0.345978042967},
{Amp: 0.0000000022287342, Period: -1.77703490, Phase: -0.345978042889},
{Amp: 0.0000000021175036, Period: 4.51107181, Phase: -0.011219786970},
{Amp: 0.0000000021175036, Period: -4.51107181, Phase: 0.011219786970},
{Amp: 0.0000000014391886, Period: -0.61965131, Phase: 0.669288476670},
{Amp: 0.0000000014391886, Period: 0.61965131, Phase: -0.669288418187},
{Amp: 0.0000000012177331, Period: 6.94927396, Phase: 0.995871908751},
{Amp: 0.0000000012177331, Period: -6.94927396, Phase: -0.995871908751},
{Amp: 0.0000000010786785, Period: 1.12776795, Phase: -0.045280251144},
{Amp: 0.0000000010786785, Period: -1.12776795, Phase: 0.045280251144},
{Amp: 0.0000000011266470, Period: 0.64099336, Phase: -0.987288717782},
{Amp: 0.0000000011266470, Period: -0.64099336, Phase: 0.987288718396},
{Amp: 0.0000000008265031, Period: -0.75178905, Phase: 2.504075950065},
{Amp: 0.0000000008265031, Period: 0.75178905, Phase: -2.504075904237},
{Amp: 0.0000000005794919, Period: -3.55143712, Phase: 3.009946487657},
{Amp: 0.0000000005794919, Period: 3.55143712, Phase: -3.009946487654},
{Amp: 0.0000000003807491, Period: 1.77777556, Phase: 1.686281241558},
{Amp: 0.0000000003807491, Period: -1.77777556, Phase: -1.686281227667},
{Amp: 0.0000000003552247, Period: -2.60797333, Phase: 0.371913588076},
{Amp: 0.0000000003552247, Period: 2.60797333, Phase: -0.371913588692},
{Amp: 0.0000000005101755, Period: -0.95555743, Phase: -1.972390618495},
{Amp: 0.0000000005101755, Period: 0.95555743, Phase: 1.972390620107},
},
{
{Amp: 0.3735263437471362, Period: 0.00000000, Phase: -3.141592653403},
{Amp: 0.0001624469912587, Period: -3.52546349, Phase: -0.248853126552},
{Amp: 0.0001624469912587, Period: 3.52546349, Phase: 0.248853148768},
{Amp: 0.0000717191594226, Period: 7.05092698, Phase: 0.909825056364},
{Amp: 0.0000717191594226, Period: -7.05092698, Phase: -0.909825056364},
{Amp: 0.0000385969570472, Period: 2.35030899, Phase: -0.412116543189},
{Amp: 0.0000385969570472, Period: -2.35030899, Phase: 0.412116578303},
{Amp: 0.0000218287365705, Period: 1.76273174, Phase: 2.068494534904},
{Amp: 0.0000218287365705, Period: -1.76273174, Phase: -2.068494534904},
{Amp: 0.0000223383238694, Period: 1.17515450, Phase: -2.395036777814},
{Amp: 0.0000223383238694, Period: -1.17515450, Phase: 2.395036777814},
{Amp: 0.0000096353043778, Period: 1.41018540, Phase: -1.734057827225},
{Amp: 0.0000096353043778, Period: -1.41018540, Phase: 1.734057862320},
{Amp: 0.0000065973457880, Period: 0.88136587, Phase: -0.574948883091},
{Amp: 0.0000065973457880, Period: -0.88136587, Phase: 0.574948883295},
{Amp: 0.0000048707009708, Period: -0.70509245, Phase: -1.160449891993},
{Amp: 0.0000048707009708, Period: 0.70509245, Phase: 1.160449916162},
{Amp: 0.0000043860283834, Period: 400.80554123, Phase: 3.108492565509},
{Amp: 0.0000043860283834, Period: -400.80554119, Phase: -3.108492540844},
{Amp: 0.0000037783019709, Period: -14526.63126285, Phase: 1.653763811537},
{Amp: 0.0000037783070079, Period: 14526.64946924, Phase: -1.653758308820},
{Amp: 0.0000052005863869, Period: 2.25553591, Phase: 1.548440639446},
{Amp: 0.0000052005863869, Period: -2.25553591, Phase: -1.548440639370},
{Amp: 0.0000033290495376, Period: -274.03305205, Phase: 2.753261920991},
{Amp: 0.0000033290495376, Period: 274.03305205, Phase: -2.753261920909},
{Amp: 0.0000029065567615, Period: -1.00727528, Phase: 3.055999599574},
{Amp: 0.0000029065567615, Period: 1.00727528, Phase: -3.055999587858},
{Amp: 0.0000032927071387, Period: -240.79911272, Phase: 2.850621646594},
{Amp: 0.0000032927071387, Period: 240.79911272, Phase: -2.850621646515},
{Amp: 0.0000027860432638, Period: 2465.81058901, Phase: 2.385773248504},
{Amp: 0.0000027860415206, Period: -2465.81062125, Phase: -2.385773134668},
{Amp: 0.0000024099254453, Period: -4.51107182, Phase: -1.559634116074},
{Amp: 0.0000024099254453, Period: 4.51107182, Phase: 1.559634116074},
{Amp: 0.0000021364215633, Period: 0.58757722, Phase: 3.051294967909},
{Amp: 0.0000021364215633, Period: -0.58757722, Phase: -3.051294932621},
{Amp: 0.0000021087772652, Period: -2082.97789867, Phase: 0.219861989989},
{Amp: 0.0000021087773173, Period: 2082.97802228, Phase: -0.219858903141},
{Amp: 0.0000018853812260, Period: 249.13840056, Phase: -2.750530921943},
{Amp: 0.0000018853812260, Period: -249.13840055, Phase: 2.750530930849},
},
{
{Amp: 0.0002988994545555, Period: 3.55118107, Phase: -0.373458788613},
{Amp: 0.0000823525166369, Period: 1.76913777, Phase: 1.446188770927},
{Amp: 0.0000837042048393, Period: -6.95025971, Phase: 1.609453836719},
{Amp: 0.0000278946698536, Period: -3.50011572, Phase: -0.871155222557},
{Amp: 0.0000315906532820, Period: 7.15455323, Phase: -2.854080409609},
{Amp: 0.0000294503681314, Period: -1.75637194, Phase: -2.690812623889},
{Amp: 0.0000144958688621, Period: -2.33901625, Phase: 2.931395664158},
{Amp: 0.0000108374431350, Period: -6.18210653, Phase: -0.351109195546},
{Amp: 0.0000082175838585, Period: -1.17232451, Phase: 1.772880355220},
{Amp: 0.0000062618381566, Period: -0.87977309, Phase: -0.057891668832},
{Amp: 0.0000041298266970, Period: -1.40611218, Phase: -2.029848170071},
{Amp: 0.0000043507065743, Period: 16.68653641, Phase: 1.973436581069},
{Amp: 0.0000042081682285, Period: 1.41426445, Phase: 3.120261383685},
{Amp: 0.0000027357551003, Period: -0.70407293, Phase: -1.868111985056},
{Amp: 0.0000036991221930, Period: 2.36171130, Phase: 2.107163763597},
{Amp: 0.0000020163445050, Period: -2.60801351, Phase: -0.434765781769},
{Amp: 0.0000026854901206, Period: 397.49502864, Phase: -2.385626020918},
{Amp: 0.0000017894118824, Period: -0.58686890, Phase: 2.596961125563},
{Amp: 0.0000023074479953, Period: 0.88296447, Phase: 1.943899853407},
{Amp: 0.0000018159137522, Period: 1.00936379, Phase: 2.604869046210},
{Amp: 0.0000011726743714, Period: -437.81569630, Phase: 1.882700621195},
{Amp: 0.0000016518864520, Period: 1.17799818, Phase: -3.017336059369},
{Amp: 0.0000018506530067, Period: 9207.93895292, Phase: -0.457770143364},
{Amp: 0.0000013196935928, Period: -1.00519540, Phase: -0.707882741855},
{Amp: 0.0000014426949422, Period: -3.55690202, Phase: -0.281777715997},
{Amp: 0.0000015660692561, Period: -548.57786416, Phase: 3.037342396932},
{Amp: 0.0000005646670312, Period: -488.13559324, Phase: 2.168360257491},
{Amp: 0.0000007495662748, Period: 3126.34340655, Phase: -0.151955418922},
{Amp: 0.0000007569857746, Period: -2303.92749369, Phase: 0.668283437067},
{Amp: 0.0000009550285338, Period: 4406.90403072, Phase: -0.300652348938},
{Amp: 0.0000007091149133, Period: -3153.42860903, Phase: 2.713933181549},
{Amp: 0.0000002004455524, Period: -1919.99999983, Phase: -1.852207712464},
{Amp: 0.0000001623489363, Period: 1408.49105459, Phase: -2.899698107364},
{Amp: 0.0000001058862562, Period: 1599.99999991, Phase: -1.747489966524},
},
{
{Amp: 0.0001662544744719, Period: 0.00000000, Phase: 2.413486237501},
{Amp: 0.0000000000608298, Period: -193.26413985, Phase: -2.230228354541},
},
},
{
{
{Amp: 0.0071566594572575, Period: 0.00000000, Phase: 0.000000000000},
{Amp: 0.0000006965149555, Period: -2.35030899, Phase: -1.158674588527},
{Amp: 0.0000006965149555, Period: 2.35030899, Phase: 1.158674600230},
{Amp: 0.0000003224914673, Period: 7.05092698, Phase: -0.660970737035},
{Amp: 0.0000003224914673, Period: -7.05092698, Phase: 0.660970737035},
{Amp: 0.0000001149029760, Period: -6.26162437, Phase: -1.299592404339},
{Amp: 0.0000001149029760, Period: 6.26162437, Phase: 1.299592404339},
{Amp: 0.0000000610717185, Period: -3.52546349, Phase: -1.819650979517},
{Amp: 0.0000000610717185, Period: 3.52546349, Phase: 1.819651032396},
{Amp: 0.0000000547899088, Period: 4.17441617, Phase: 1.948670877782},
{Amp: 0.0000000547899088, Period: -4.17441617, Phase: -1.948670828180},
{Amp: 0.0000000350717808, Period: -12.52324882, Phase: -0.649785081154},
{Amp: 0.0000000350717808, Period: 12.52324882, Phase: 0.649785104562},
{Amp: 0.0000000273934283, Period: -3.13081218, Phase: -2.599205067142},
{Amp: 0.0000000273934283, Period: 3.13081218, Phase: 2.599205102257},
{Amp: 0.0000000181610714, Period: -1.76273174, Phase: -0.497708209553},
{Amp: 0.0000000181610714, Period: 1.76273174, Phase: 0.497708210129},
{Amp: 0.0000000197317929, Period: 1.17515450, Phase: -0.824239093090},
{Amp: 0.0000000197317929, Period: -1.17515450, Phase: 0.824239093704},
{Amp: 0.0000000140622484, Period: -2.50464974, Phase: 3.034184231000},
{Amp: 0.0000000140622484, Period: 2.50464974, Phase: -3.034184231000},
{Amp: 0.0000000103962349, Period: 1.41018540, Phase: -0.163261763515},
{Amp: 0.0000000103962349, Period: -1.41018540, Phase: 0.163261798610},
{Amp: 0.0000000145474682, Period: -3.58319300, Phase: -2.012339222997},
{Amp: 0.0000000145474682, Period: 3.58319300, Phase: 2.012339225153},
{Amp: 0.0000000073448387, Period: 2.08720812, Phase: -2.384360905770},
{Amp: 0.0000000073448387, Period: -2.08720812, Phase: 2.384360906102},
{Amp: 0.0000000059965021, Period: -1.00727528, Phase: 1.485195822039},
{Amp: 0.0000000059965021, Period: 1.00727528, Phase: -1.485195786944},
{Amp: 0.0000000049933886, Period: 10.02108773, Phase: 0.967007202582},
{Amp: 0.0000000049933886, Period: -10.02108773, Phase: -0.967007214068},
{Amp: 0.0000000038834130, Period: 1.78903553, Phase: -1.734548005475},
{Amp: 0.0000000038834130, Period: -1.78903553, Phase: 1.734548012548},
{Amp: 0.0000000033173319, Period: 0.78343633, Phase: 0.334410735342},
{Amp: 0.0000000033173319, Period: -0.78343633, Phase: -0.334410703853},
{Amp: 0.0000000037071986, Period: 50.13968535, Phase: 0.161404498578},
{Amp: 0.0000000037071986, Period: -50.13968535, Phase: -0.161404499697},
{Amp: 0.0000000024697553, Period: 3.58615746, Phase: 3.014796806223},
{Amp: 0.0000000024697553, Period: -3.58615746, Phase: -3.014796771059},
{Amp: 0.0000000027884176, Period: 5.56655476, Phase: 1.323585106216},
{Amp: 0.0000000027884176, Period: -5.56655476, Phase: -1.323585106216},
{Amp: 0.0000000020719852, Period: 1.56540608, Phase: -1.084729476400},
{Amp: 0.0000000020719852, Period: -1.56540608, Phase: 1.084729523216},
{Amp: 0.0000000018431031, Period: 0.58757726, Phase: 1.497090964345},
{Amp: 0.0000000018431031, Period: -0.58757726, Phase: -1.497090952676},
{Amp: 0.0000000016808769, Period: 7.15554098, Phase: -2.533819463034},
{Amp: 0.0000000016808769, Period: -7.15554098, Phase: 2.533819476120},
{Amp: 0.0000000020382815, Period: -0.88136587, Phase: 2.146155229321},
{Amp: 0.0000000020382815, Period: 0.88136587, Phase: -2.146155229461},
{Amp: 0.0000000016674142, Period: 3.85367596, Phase: 2.266819681887},
{Amp: 0.0000000016674142, Period: -3.85367596, Phase: -2.266819678138},
{Amp: 0.0000000012877349, Period: -3.49986546, Phase: 2.808660684496},
{Amp: 0.0000000012877349, Period: 3.49986546, Phase: -2.808660671629},
{Amp: 0.0000000012181542, Period: 55.93578462, Phase: 1.960483840747},
{Amp: 0.0000000012181542, Period: -55.93578462, Phase: -1.960483829044},
{Amp: 0.0000000010016338, Period: -2.94686240, Phase: -2.916664806186},
{Amp: 0.0000000010016338, Period: 2.94686240, Phase: 2.916664806820},
{Amp: 0.0000000009057853, Period: -6.94927480, Phase: 2.143765079507},
{Amp: 0.0000000009057853, Period: 6.94927480, Phase: -2.143765093634},
{Amp: 0.0000000011132716, Period: -1.39147207, Phase: 0.434910736795},
{Amp: 0.0000000011132716, Period: 1.39147207, Phase: -0.434910734723},
{Amp: 0.0000000007267503, Period: 0.70508749, Phase: 1.874821204178},
{Amp: 0.0000000007267503, Period: -0.70508749, Phase: -1.874821204178},
},
{
{Amp: 0.2874089391143348, Period: 0.00000000, Phase: 0.000000000000},
{Amp: 0.0000581860484889, Period: -2.35030899, Phase: 0.412122994161},
{Amp: 0.0000581860484889, Period: 2.35030899, Phase: -0.412122959047},
{Amp: 0.0000407623427232, Period: -7.05092698, Phase: 2.231767920686},
{Amp: 0.0000407623427232, Period: 7.05092698, Phase: -2.231767920577},
{Amp: 0.0000400609839801, Period: 6.26162437, Phase: 2.870388622006},
{Amp: 0.0000400609839801, Period: -6.26162437, Phase: -2.870388622006},
{Amp: 0.0000303508630091, Period: 12.52324880, Phase: 2.220584023571},
{Amp: 0.0000303508630091, Period: -12.52324880, Phase: -2.220583988461},
{Amp: 0.0000213787490768, Period: 3.52546349, Phase: -2.892737639574},
{Amp: 0.0000213787490768, Period: -3.52546349, Phase: 2.892737684197},
{Amp: 0.0000153680208913, Period: 4.17441628, Phase: -2.762551710070},
{Amp: 0.0000153680208913, Period: -4.17441628, Phase: 2.762551710070},
{Amp: 0.0000067324310952, Period: -3.13081218, Phase: 2.113191687546},
{Amp: 0.0000067324310952, Period: 3.13081218, Phase: -2.113191675837},
{Amp: 0.0000072634431824, Period: -50.14182397, Phase: -1.759435081522},
{Amp: 0.0000072634431824, Period: 50.14182397, Phase: 1.759435084589},
{Amp: 0.0000043977562585, Period: -1.76273174, Phase: -2.068506076335},
{Amp: 0.0000043977562585, Period: 1.76273174, Phase: 2.068506076335},
{Amp: 0.0000047762008660, Period: 1.17515450, Phase: 0.746555718634},
{Amp: 0.0000047762008660, Period: -1.17515450, Phase: -0.746555718634},
{Amp: 0.0000031610812971, Period: -2.50464974, Phase: 1.463376735121},
{Amp: 0.0000031610812971, Period: 2.50464974, Phase: -1.463376734852},
{Amp: 0.0000035573097979, Period: -3.58319294, Phase: 2.700332818172},
{Amp: 0.0000035573097979, Period: 3.58319294, Phase: -2.700332827650},
{Amp: 0.0000023282599410, Period: 14399.99998683, Phase: -1.099877635453},
{Amp: 0.0000023282599359, Period: -14399.99998683, Phase: 1.099877639624},
{Amp: 0.0000023502114735, Period: -10.02107511, Phase: -2.536966831342},
{Amp: 0.0000023502114735, Period: 10.02107511, Phase: 2.536966842808},
{Amp: 0.0000023510383997, Period: 1.41018540, Phase: 1.407535452307},
{Amp: 0.0000023510383997, Period: -1.41018540, Phase: -1.407535440592},
{Amp: 0.0000021174661004, Period: 55.93578577, Phase: -2.751914513472},
{Amp: 0.0000021174661004, Period: -55.93578577, Phase: 2.751914548581},
{Amp: 0.0000016288866844, Period: -3679.84840303, Phase: -0.973793744174},
{Amp: 0.0000016288866845, Period: 3679.84840303, Phase: 0.973793744140},
{Amp: 0.0000019377870959, Period: -249.86158491, Phase: 2.450426504098},
{Amp: 0.0000019377870959, Period: 249.86158492, Phase: -2.450426503434},
},
{
{Amp: 0.0002045597496146, Period: -50.17098410, Phase: -1.011816940225},
{Amp: 0.0001785118648258, Period: 7.15455319, Phase: 0.287431567198},
{Amp: 0.0001131999784893, Period: 1.76913777, Phase: 1.446212727724},
{Amp: 0.0000658778169210, Period: -3.50011574, Phase: -0.871350254578},
{Amp: 0.0000497058888328, Period: 3.55118106, Phase: -0.373506086712},
{Amp: 0.0000316384926978, Period: -6.95025977, Phase: -1.532287159698},
{Amp: 0.0000287801237327, Period: -10.02171452, Phase: -1.661453180348},
{Amp: 0.0000181744317896, Period: 16.68901713, Phase: 2.779471484341},
{Amp: 0.0000105558175161, Period: -5.56684974, Phase: -2.311166167228},
{Amp: 0.0000057610853129, Period: -1.40611219, Phase: 1.111461108666},
{Amp: 0.0000061046181888, Period: -7.17825897, Phase: -1.726424077467},
{Amp: 0.0000057310334964, Period: -2.33901627, Phase: -0.210412331329},
{Amp: 0.0000046610005483, Period: -3.85376806, Phase: -2.960887284255},
{Amp: 0.0000034982417330, Period: 3.33918688, Phase: 1.586656801126},
{Amp: 0.0000030091617315, Period: -1.75637195, Phase: 0.450524745204},
{Amp: 0.0000024732926446, Period: 25.01011480, Phase: 2.204580355873},
{Amp: 0.0000024171568015, Period: 0.00000000, Phase: -2.832382068191},
{Amp: 0.0000022247695560, Period: -2.94691618, Phase: 2.672542463618},
{Amp: 0.0000020947921969, Period: 2.63625763, Phase: 2.235037411637},
{Amp: 0.0000024416432533, Period: -25.08296275, Phase: -1.578237604387},
{Amp: 0.0000014042712722, Period: 4.55326510, Phase: -2.204412209524},
{Amp: 0.0000011180389240, Period: -1.17232452, Phase: 1.772399331347},
{Amp: 0.0000010371714944, Period: -7.19017077, Phase: -2.734365259635},
{Amp: 0.0000011076384510, Period: -2.38555726, Phase: 2.022753854104},
{Amp: 0.0000011932531874, Period: 2.17780908, Phase: 2.886194141381},
{Amp: 0.0000007178049665, Period: 2.36171130, Phase: 2.107149695705},
{Amp: 0.0000006908412319, Period: -8.35287888, Phase: -2.223571888688},
{Amp: 0.0000006659820028, Period: 1.85518927, Phase: -2.747232277664},
{Amp: 0.0000006772314920, Period: 2.38747924, Phase: 2.301347989676},
{Amp: 0.0000008993128501, Period: -0.87977305, Phase: -0.047323465226},
{Amp: 0.0000006286307601, Period: 0.88296448, Phase: -1.197531764168},
{Amp: 0.0000006784151570, Period: 1478.51706690, Phase: 0.743089169080},
{Amp: 0.0000005660807396, Period: -2.00384437, Phase: 1.372931645641},
{Amp: 0.0000006339665249, Period: 1.41428228, Phase: 0.785273916249},
{Amp: 0.0000006128705113, Period: 1.00936322, Phase: -0.638851146096},
{Amp: 0.0000005206551413, Period: -1.00519540, Phase: 2.433337444677},
{Amp: 0.0000004583970422, Period: -0.64015046, Phase: -1.206485628814},
{Amp: 0.0000003466029660, Period: 397.49139960, Phase: 0.754124274965},
{Amp: 0.0000004577854173, Period: 3.57743657, Phase: 3.093485444125},
{Amp: 0.0000004718481418, Period: 37429.59476194, Phase: 1.395067854967},
},
{
{Amp: 0.0008533093128905, Period: 0.00000000, Phase: 2.413388168755},
{Amp: 0.0000000926213408, Period: -243.69680308, Phase: -0.936338792860},
{Amp: 0.0000000000106902, Period: -192.66710225, Phase: -1.706763976490},
},
},
{
{
{Amp: 0.0125879701715314, Period: 0.00000000, Phase: 0.000000000000},
{Amp: 0.0000013790105326, Period: -1.97891484, Phase: -1.808423578125},
{Amp: 0.0000013790105326, Period: 1.97891484, Phase: 1.808423589815},
{Amp: 0.0000017976024735, Period: 12.52324852, Phase: 0.649657760137},
{Amp: 0.0000017976024735, Period: -12.52324852, Phase: -0.649657760137},
{Amp: 0.0000006437448086, Period: -4.51107179, Phase: 0.011294478638},
{Amp: 0.0000006437448086, Period: 4.51107179, Phase: -0.011294478574},
{Amp: 0.0000002086864553, Period: -6.26162422, Phase: 1.842527624347},
{Amp: 0.0000002086864553, Period: 6.26162422, Phase: -1.842527612645},
{Amp: 0.0000001395378859, Period: 8.37677335, Phase: 0.714288700422},
{Amp: 0.0000001395378859, Period: -8.37677335, Phase: -0.714288676300},
{Amp: 0.0000000999126129, Period: 4.17441617, Phase: -1.192604552413},
{Amp: 0.0000000999126129, Period: -4.17441617, Phase: 1.192604552682},
{Amp: 0.0000000500574919, Period: -3.13081213, Phase: 0.542875780384},
{Amp: 0.0000000500574919, Period: 3.13081213, Phase: -0.542875768668},
{Amp: 0.0000000256983546, Period: 2.50464971, Phase: 0.106887217028},
{Amp: 0.0000000256983546, Period: -2.50464971, Phase: -0.106887217655},
{Amp: 0.0000000237843996, Period: -8.39299929, Phase: -1.721959538178},
{Amp: 0.0000000237843996, Period: 8.39299929, Phase: 1.721959573291},
{Amp: 0.0000000174121120, Period: 16.69021715, Phase: 0.150827135054},
{Amp: 0.0000000174121120, Period: -16.69021715, Phase: -0.150827135054},
{Amp: 0.0000000119553173, Period: 10.02131069, Phase: -1.925833362861},
{Amp: 0.0000000119553173, Period: -10.02131069, Phase: 1.925833397083},
{Amp: 0.0000000131617319, Period: -2.08719489, Phase: -0.208389567476},
{Amp: 0.0000000131617319, Period: 2.08719489, Phase: 0.208389567476},
{Amp: 0.0000000141920315, Period: -50.14242063, Phase: 1.115887970655},
{Amp: 0.0000000141920315, Period: 50.14242063, Phase: -1.115887945684},
{Amp: 0.0000000109988711, Period: 2.24510856, Phase: 1.507540480873},
{Amp: 0.0000000109988711, Period: -2.24510856, Phase: -1.507540445897},
{Amp: 0.0000000085572239, Period: -2.25553590, Phase: -3.119143532879},
{Amp: 0.0000000085572239, Period: 2.25553590, Phase: 3.119143533646},
{Amp: 0.0000000070978417, Period: -1.78903551, Phase: -1.406515118334},
{Amp: 0.0000000070978417, Period: 1.78903551, Phase: 1.406515133811},
{Amp: 0.0000000060001815, Period: -5.56672420, Phase: 1.283203905605},
{Amp: 0.0000000060001815, Period: 5.56672420, Phase: -1.283203904944},
{Amp: 0.0000000054209452, Period: -0.93228920, Phase: 0.795871427118},
{Amp: 0.0000000054209452, Period: 0.93228920, Phase: -0.795871427118},
{Amp: 0.0000000054109127, Period: 6.18191832, Phase: -0.305922255492},
{Amp: 0.0000000054109127, Period: -6.18191832, Phase: 0.305922255492},
},
{
{Amp: 0.3620341291375704, Period: 0.00000000, Phase: -3.141592653590},
{Amp: 0.0001102576431631, Period: -12.52324875, Phase: 0.920999800869},
{Amp: 0.0001102576431631, Period: 12.52324875, Phase: -0.920999800869},
{Amp: 0.0000938947575579, Period: 1.97891484, Phase: 0.237682433552},
{Amp: 0.0000938947575579, Period: -1.97891484, Phase: -0.237682433079},
{Amp: 0.0000383458487621, Period: 4.51107181, Phase: -1.581970202032},
{Amp: 0.0000383458487621, Period: -4.51107181, Phase: 1.581970202035},
{Amp: 0.0000373528427553, Period: 6.26162437, Phase: -0.271204706549},
{Amp: 0.0000373528427553, Period: -6.26162437, Phase: 0.271204706549},
{Amp: 0.0000194161648683, Period: 8.37677211, Phase: 2.283688674383},
{Amp: 0.0000194161648683, Period: -8.37677211, Phase: -2.283688674381},
{Amp: 0.0000146516338969, Period: -4.17441624, Phase: -0.378597607197},
{Amp: 0.0000146516338969, Period: 4.17441624, Phase: 0.378597607197},
{Amp: 0.0000064963948341, Period: 3.13081218, Phase: 1.028401028095},
{Amp: 0.0000064963948341, Period: -3.13081218, Phase: -1.028401016392},
{Amp: 0.0000074412818628, Period: -50.14197245, Phase: 1.380281518586},
{Amp: 0.0000074412818628, Period: 50.14197245, Phase: -1.380281518587},
{Amp: 0.0000032693721243, Period: 3796.36874430, Phase: 1.987215709322},
{Amp: 0.0000032693721243, Period: -3796.36874429, Phase: -1.987215709307},
{Amp: 0.0000033105851447, Period: -8.39299149, Phase: 2.997485814677},
{Amp: 0.0000033105851447, Period: 8.39299149, Phase: -2.997485813557},
{Amp: 0.0000030790399070, Period: 2.50464974, Phase: 1.678207462150},
{Amp: 0.0000030790399070, Period: -2.50464974, Phase: -1.678207462150},
{Amp: 0.0000023398570389, Period: -10.02107449, Phase: 0.604674627622},
{Amp: 0.0000023398570389, Period: 10.02107449, Phase: -0.604674593224},
},
{
{Amp: 0.0002065924169942, Period: 16.68901704, Phase: -0.362202150664},
{Amp: 0.0001589869764021, Period: 7.15455319, Phase: 0.287440062412},
{Amp: 0.0001486043380971, Period: 1.76913777, Phase: 1.446213430046},
{Amp: 0.0000635073108731, Period: 3.55118106, Phase: -0.373504978601},
{Amp: 0.0000599351698525, Period: -2.24513352, Phase: -2.170633548755},
{Amp: 0.0000489596900866, Period: -10.02171449, Phase: 1.480222294735},
{Amp: 0.0000333682283528, Period: -16.81857729, Phase: -1.076487783264},
{Amp: 0.0000292325461337, Period: -50.17099062, Phase: -1.012422966902},
{Amp: 0.0000295832427279, Period: -6.18210646, Phase: -0.350915517480},
{Amp: 0.0000197588369441, Period: 0.00000000, Phase: -2.951408504882},
{Amp: 0.0000183551029746, Period: -5.56684974, Phase: 0.830451671341},
{Amp: 0.0000081987970452, Period: -3.85376806, Phase: 0.180738718447},
{Amp: 0.0000039403527376, Period: -2.94691618, Phase: -0.469009428127},
{Amp: 0.0000056895636122, Period: -16.88411373, Phase: -2.084089640414},
{Amp: 0.0000040434854859, Period: -25.08297371, Phase: 1.559247963578},
{Amp: 0.0000036901291978, Period: 5.57732714, Phase: 0.352077722692},
{Amp: 0.0000019322089806, Period: 25.00957616, Phase: -1.141325761390},
{Amp: 0.0000019711212463, Period: -2.38555726, Phase: -1.118790685515},
{Amp: 0.0000018673159813, Period: 4.55326506, Phase: -2.204843732624},
{Amp: 0.0000016695689644, Period: 3.33918689, Phase: -1.554811604707},
{Amp: 0.0000014034621874, Period: -2.60801097, Phase: 2.801758550422},
{Amp: 0.0000011758260607, Period: -8.35287933, Phase: 0.916517243491},
{Amp: 0.0000016838424078, Period: 8.34481080, Phase: -0.203581964410},
{Amp: 0.0000010798624964, Period: 2.63625769, Phase: -0.905106521704},
{Amp: 0.0000010108880552, Period: -2.00384437, Phase: -1.768605450166},
{Amp: 0.0000008876681807, Period: -12.52396907, Phase: 1.911818107846},
{Amp: 0.0000008194699011, Period: -16.75354536, Phase: 3.077495182201},
{Amp: 0.0000006728737059, Period: -16.95016232, Phase: -3.092183700860},
{Amp: 0.0000006297345982, Period: 5.58451544, Phase: 1.359571973412},
{Amp: 0.0000006128899757, Period: 0.88296471, Phase: -1.142969177277},
{Amp: 0.0000007093782158, Period: -2.59408012, Phase: -1.871354043032},
{Amp: 0.0000005580987049, Period: -5.01075568, Phase: 0.270180657271},
},
{
{Amp: 0.0038422977898495, Period: 0.00000000, Phase: 2.413392208631},
{Amp: 0.0000000000666922, Period: -192.00000000, Phase: -2.221562730997},
},
},
}
var jupiterGalileanL1Chebyshev = [4][5][9]float64{
{
{8.26698820334074e-005, -5.46986318473484e-006, 5.94965634346142e-005, -1.10076009434021e-005, -5.75048718532862e-006, 5.74994024502453e-006, 1.78506935927118e-006, -2.40677412003469e-006, -1.38513209122323e-006},
{-1.33581115495368e-007, -7.58579668623469e-009, -1.73800453537518e-009, -3.02381800276162e-009, -7.21047445746571e-009, -3.92259073275152e-010, -4.16879673796892e-009, -3.29083173926104e-009, 2.88537255009037e-009},
{-4.47550080805276e-008, -5.78177122641661e-010, -5.35142525086474e-009, 1.02942385189608e-010, -8.49040607452073e-009, -2.52840670456370e-009, -6.13405545173217e-009, -3.43800069555589e-009, -3.59984191898440e-009},
{-2.91234929817703e-007, -1.62905002759586e-007, -3.68431020234090e-009, 7.77164290527916e-010, 1.59932283427177e-009, -2.28491296590504e-009, 7.03033944342130e-010, -2.89289190860300e-011, 4.86451562749639e-010},
{-3.17117027043262e-007, -1.26321744097649e-007, 6.51758518535435e-009, 2.62070675646012e-009, 2.59238370832140e-009, -7.26318061740534e-010, -1.18191642174809e-009, 5.90816038543625e-010, 1.23053999757825e-009},
},
{
{-2.34691437046728e-004, 2.59036121717763e-005, -1.73835841495123e-004, 3.80673357683932e-005, -1.03485888270120e-007, 1.17376776995138e-005, 3.19509608989603e-006, -4.83167496026174e-006, 4.47094431509368e-007},
{-1.00355398526925e-006, -5.80438343691240e-008, 3.80875695145265e-008, -2.10657418625019e-007, 3.12238251072476e-007, -1.72444072203130e-007, 2.35985832098386e-007, 6.13727340553035e-008, -1.58131098756712e-007},
{-2.61024960215820e-007, -1.82656275716032e-007, 2.78016374834476e-008, -1.58858363289506e-007, 2.31850746815330e-008, -1.10691155968765e-007, -3.69048459423356e-008, 6.78631069587260e-009, 1.90777145516695e-008},
{-2.74704767394256e-006, -1.54365304913777e-006, -3.49546762061882e-008, 2.86789414864130e-009, 1.75452766852069e-008, -1.97744662243578e-008, 3.43169676541053e-009, -1.30924950693160e-010, 1.35338072425105e-008},
{-2.98415696654187e-006, -1.19708851225481e-006, 5.67146658521585e-008, 2.68814727457189e-008, 2.61897719058852e-008, -5.11649608698392e-009, -1.27841777400320e-008, 1.19534263567129e-008, 1.15749543781328e-008},
},
{
{-4.02499890223698e-004, 4.22870514937427e-005, -2.88258753042082e-004, 6.23338370193608e-005, 1.78692873660163e-006, 1.44147643545141e-005, 6.58944353306297e-006, -6.19139500170464e-006, 7.82954615405540e-007},
{-1.11797459279082e-007, -2.78967292638245e-007, -7.04645336679784e-008, -2.38693040681653e-007, 1.22869992713930e-008, -1.53370399783488e-007, 4.47318789843126e-008, -1.65802124252890e-008, -8.28946056921499e-008},
{-1.07044201697345e-007, -2.23668695371038e-007, -2.18527598721759e-007, -2.00040954734845e-007, -3.19586844323525e-007, -2.22086683443490e-007, -2.98609138042077e-007, -1.29217448782639e-007, -1.50654590880644e-007},
{-1.41968295962213e-005, -7.93995493982020e-006, -2.08882224662785e-007, 2.09762683478441e-008, 8.35070484335148e-008, -1.07729670354460e-007, 3.06301230195125e-008, 1.12449433356374e-008, 7.08786417409988e-009},
{-1.53976109321613e-005, -6.12022141299496e-006, 3.15217558237908e-007, 1.48179418616661e-007, 1.41150094231365e-007, -2.24063936978287e-008, -3.33311708439601e-008, 1.07863442857316e-008, 6.62499119729457e-008},
},
{
{3.06204347064615e-004, -1.68321429486551e-004, 2.02182016979082e-004, -1.37641833741129e-004, -5.30404402536766e-005, -1.10474926063654e-005, -1.05634674903822e-005, 3.11526543050599e-005, 4.66495863544146e-006},
{-1.81310025984216e-007, -1.26014618556329e-006, -3.82306332383069e-007, -6.67102945644653e-007, -5.05214970641581e-008, 1.53918794369926e-007, 9.28279781805284e-008, 3.66048038576706e-007, 5.68038769950531e-008},
{-1.40782617429892e-006, -1.35394137498700e-006, -3.20084097458594e-007, -6.20618627039507e-007, -6.11742588608485e-007, -2.76537195728238e-007, -1.33810977431205e-007, 2.89677483700145e-007, 4.70713328715423e-008},
{-6.40936655172450e-005, -3.57948862513955e-005, -8.87261624128964e-007, 1.64506004407617e-007, 4.77415826267116e-007, -3.59030239515192e-007, 1.17406290303369e-007, 8.95051737231057e-008, 9.71875658565188e-009},
{-6.96069435911693e-005, -2.76636397672750e-005, 1.38328020437413e-006, 6.51631405812798e-007, 6.20307849865976e-007, -2.66081185506962e-007, -2.09003718083271e-007, -1.46372661194868e-007, -1.22390974721364e-007},
},
}
+201
View File
@@ -0,0 +1,201 @@
package basic
import "math"
const solarRadiusAU = 695700.0 / astronomicalUnitKM
// JupiterGalileanPhenomenon 木星伽利略卫星瞬时现象 / instantaneous Galilean-satellite phenomena.
//
// Transit 表示卫星本体在木星盘前;Occultation 表示卫星在木星盘后被掩蔽;Eclipse 表示卫星落入木星本影;ShadowTransit 表示卫星影心落在可见木星盘面上。
// Transit means the satellite itself is in front of Jupiter's disk; Occultation means it is hidden behind the disk; Eclipse means the satellite lies in Jupiter's umbra; ShadowTransit means the center of the satellite shadow falls on the visible Jovian disk.
type JupiterGalileanPhenomenon struct {
Transit bool
Occultation bool
Eclipse bool
ShadowTransit bool
ShadowOffsetXArcsec float64
ShadowOffsetYArcsec float64
ShadowOffsetXJupiterRadii float64
ShadowOffsetYJupiterRadii float64
}
// JupiterGalileanSatellitePhenomenon 单颗伽利略卫星瞬时现象 / instantaneous phenomena of one Galilean satellite.
func JupiterGalileanSatellitePhenomenon(jd float64, satellite int) JupiterGalileanPhenomenon {
if satellite < 1 || satellite > 4 || !isFinite(jd) {
return invalidJupiterGalileanPhenomenon()
}
context := newJupiterGalileanObservationContext(jd)
return context.phenomenonForSatellite(satellite - 1)
}
// JupiterGalileanSatellitePhenomena 四颗伽利略卫星瞬时现象 / instantaneous phenomena of the four Galilean satellites.
func JupiterGalileanSatellitePhenomena(jd float64) [4]JupiterGalileanPhenomenon {
var phenomena [4]JupiterGalileanPhenomenon
context := newJupiterGalileanObservationContext(jd)
for i := range phenomena {
phenomena[i] = context.phenomenonForSatellite(i)
}
return phenomena
}
func (context jupiterGalileanObservationContext) phenomenonForSatellite(index int) JupiterGalileanPhenomenon {
if index < 0 || index >= 4 || context.jupiterDistance == 0 {
return invalidJupiterGalileanPhenomenon()
}
observation := context.observationForSatellite(index)
stateVector := Vector3{observation.State.X, observation.State.Y, observation.State.Z}
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
xEarth := observation.OffsetXJupiterRadii
yEarth := observation.OffsetYJupiterRadii
onEarthDisk := ellipseInside(xEarth, yEarth, 1, context.earthMinorRadius)
xSunAU := vectorDot(stateVector, context.sunEast)
ySunAU := vectorDot(stateVector, context.sunNorth)
zSunAU := vectorDot(stateVector, context.sunLineOfSight)
xSun := xSunAU / radiusAU
ySun := ySunAU / radiusAU
umbraScale := jupiterUmbraScale(zSunAU, context.sunDistanceAU)
eclipse := false
if zSunAU > 0 && umbraScale > 0 {
eclipse = ellipseInside(xSun, ySun, umbraScale, context.sunMinorRadius*umbraScale)
}
shadowTransit, shadowXAU, shadowYAU := context.shadowTransitFor(stateVector)
phenomenon := JupiterGalileanPhenomenon{
Transit: onEarthDisk && observation.InFrontOfJupiter,
Occultation: onEarthDisk && !observation.InFrontOfJupiter,
Eclipse: eclipse,
ShadowTransit: shadowTransit,
ShadowOffsetXArcsec: math.NaN(),
ShadowOffsetYArcsec: math.NaN(),
ShadowOffsetXJupiterRadii: math.NaN(),
ShadowOffsetYJupiterRadii: math.NaN(),
}
if shadowTransit {
phenomenon.ShadowOffsetXArcsec = math.Atan2(shadowXAU, context.jupiterDistance) * deg * 3600
phenomenon.ShadowOffsetYArcsec = math.Atan2(shadowYAU, context.jupiterDistance) * deg * 3600
phenomenon.ShadowOffsetXJupiterRadii = shadowXAU / radiusAU
phenomenon.ShadowOffsetYJupiterRadii = shadowYAU / radiusAU
}
return phenomenon
}
func (context jupiterGalileanObservationContext) shadowTransitFor(stateVector Vector3) (bool, float64, float64) {
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
satelliteBody := context.toBodyCoordinates(stateVector)
satelliteBody = Vector3{satelliteBody[0] / radiusAU, satelliteBody[1] / radiusAU, satelliteBody[2] / radiusAU}
directionBody := context.toBodyCoordinates(context.sunLineOfSight)
intersectionBody, ok := ellipsoidRayIntersection(satelliteBody, directionBody, jupiterPolarRadiusRatio())
if !ok {
return false, 0, 0
}
normalBody := Vector3{intersectionBody[0], intersectionBody[1], intersectionBody[2] / (jupiterPolarRadiusRatio() * jupiterPolarRadiusRatio())}
earthBody := context.toBodyCoordinates(context.earthDirection)
if vectorDot(normalBody, earthBody) <= 0 {
return false, 0, 0
}
intersection := context.fromBodyCoordinates(Vector3{
intersectionBody[0] * radiusAU,
intersectionBody[1] * radiusAU,
intersectionBody[2] * radiusAU,
})
xAU := vectorDot(intersection, context.east)
yAU := vectorDot(intersection, context.north)
x := xAU / radiusAU
y := yAU / radiusAU
if !ellipseInside(x, y, 1, context.earthMinorRadius) {
return false, 0, 0
}
return true, xAU, yAU
}
func (context jupiterGalileanObservationContext) toBodyCoordinates(vector Vector3) Vector3 {
return Vector3{
vectorDot(vector, context.bodyX),
vectorDot(vector, context.bodyY),
vectorDot(vector, context.bodyZ),
}
}
func (context jupiterGalileanObservationContext) fromBodyCoordinates(vector Vector3) Vector3 {
return Vector3{
context.bodyX[0]*vector[0] + context.bodyY[0]*vector[1] + context.bodyZ[0]*vector[2],
context.bodyX[1]*vector[0] + context.bodyY[1]*vector[1] + context.bodyZ[1]*vector[2],
context.bodyX[2]*vector[0] + context.bodyY[2]*vector[1] + context.bodyZ[2]*vector[2],
}
}
func jupiterProjectedMinorRadius(direction, pole Vector3) float64 {
sinBeta := vectorDot(direction, pole)
cos2Beta := 1 - sinBeta*sinBeta
if cos2Beta < 0 {
cos2Beta = 0
}
ratio := jupiterPolarRadiusRatio()
return math.Sqrt(sinBeta*sinBeta + ratio*ratio*cos2Beta)
}
func jupiterPolarRadiusRatio() float64 {
return jupiterPhysicalModel.polarRadius / jupiterPhysicalModel.equatorialRadius
}
func ellipseInside(x, y, major, minor float64) bool {
if major <= 0 || minor <= 0 {
return false
}
return (x*x)/(major*major)+(y*y)/(minor*minor) <= 1+1e-12
}
func ellipsoidRayIntersection(origin, direction Vector3, polarRatio float64) (Vector3, bool) {
invPolar2 := 1 / (polarRatio * polarRatio)
a := direction[0]*direction[0] + direction[1]*direction[1] + direction[2]*direction[2]*invPolar2
b := 2 * (origin[0]*direction[0] + origin[1]*direction[1] + origin[2]*direction[2]*invPolar2)
c := origin[0]*origin[0] + origin[1]*origin[1] + origin[2]*origin[2]*invPolar2 - 1
discriminant := b*b - 4*a*c
if discriminant < 0 {
return Vector3{}, false
}
sqrtDiscriminant := math.Sqrt(discriminant)
t1 := (-b - sqrtDiscriminant) / (2 * a)
t2 := (-b + sqrtDiscriminant) / (2 * a)
t := math.Inf(1)
if t1 > 0 {
t = t1
}
if t2 > 0 && t2 < t {
t = t2
}
if !isFinite(t) {
return Vector3{}, false
}
return Vector3{
origin[0] + t*direction[0],
origin[1] + t*direction[1],
origin[2] + t*direction[2],
}, true
}
func jupiterUmbraScale(distanceBehindAU, sunDistanceAU float64) float64 {
if distanceBehindAU <= 0 || sunDistanceAU <= 0 {
return 0
}
jupiterRadiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
umbraLength := jupiterRadiusAU * sunDistanceAU / (solarRadiusAU - jupiterRadiusAU)
if umbraLength <= 0 {
return 0
}
return 1 - distanceBehindAU/umbraLength
}
func invalidJupiterGalileanPhenomenon() JupiterGalileanPhenomenon {
nan := math.NaN()
return JupiterGalileanPhenomenon{
ShadowOffsetXArcsec: nan,
ShadowOffsetYArcsec: nan,
ShadowOffsetXJupiterRadii: nan,
ShadowOffsetYJupiterRadii: nan,
}
}
+424
View File
@@ -0,0 +1,424 @@
package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
const (
jupiterGalileanReferenceJD = 2433282.5
jupiterGalileanLongPeriodShift = 310910.16
jupiterGalileanMinSolarLPYear = 1150.0
jupiterGalileanMaxSolarLPYear = 2750.0
jupiterGalileanEquatorialRadiusKM = 71492.0
astronomicalUnitKM = 149597870.691
)
var (
jupiterGalileanBaseMeanLongitudes = [4]float64{3.55155228618240, 1.76932271112347, 0.878207923589328, 0.376486233433828}
jupiterGalileanMu = [4]float64{2.82489428433814e-07, 2.82483274392893e-07, 2.82498184184723e-07, 2.82492144889909e-07}
)
const (
jupiterGalileanFrameNode = 6.24950183065715
jupiterGalileanFrameTilt = 0.445094736497665
)
type jupiterGalileanL1Term struct {
Amp float64
Period float64
Phase float64
}
// JupiterGalileanState 木星伽利略卫星原始状态 / raw Galilean-satellite state.
//
// 输入 jd 使用 TT/TDB 对应的儒略日;返回值为 IMCCE L1 理论的木心 J2000 平赤道直角坐标与速度,单位 AU / AU/day。
// The input jd is a TT/TDB Julian day. Returned coordinates are Jovicentric J2000 mean-equatorial position and velocity from the IMCCE L1 theory, in AU and AU/day.
type JupiterGalileanState struct {
X float64
Y float64
Z float64
VX float64
VY float64
VZ float64
}
// JupiterGalileanObservation 木星伽利略卫星视位置 / apparent Galilean-satellite geometry.
//
// 视位置相对木星中心定义:X 向天球东为正,Y 向天球北为正,Z>0 表示比木星更远、位于盘后。
// Apparent offsets are relative to Jupiter's center: X is positive to celestial east, Y to celestial north, and Z>0 means farther than Jupiter and behind the disk.
type JupiterGalileanObservation struct {
State JupiterGalileanState
RA float64
Dec float64
Distance float64
OffsetXArcsec float64
OffsetYArcsec float64
OffsetXJupiterRadii float64
OffsetYJupiterRadii float64
OffsetZJupiterRadii float64
InFrontOfJupiter bool
}
// JupiterGalileanSatelliteState 伽利略卫星木心 J2000 状态 / Jovicentric J2000 state of a Galilean satellite.
//
// satellite 取 1=Io, 2=Europa, 3=Ganymede, 4=Callisto。jd 为 TT/TDB 对应儒略日。
// satellite is 1=Io, 2=Europa, 3=Ganymede, 4=Callisto. jd is a TT/TDB Julian day.
func JupiterGalileanSatelliteState(jd float64, satellite int) JupiterGalileanState {
if satellite < 1 || satellite > 4 || !isFinite(jd) {
return invalidJupiterGalileanState()
}
et := jd - jupiterGalileanReferenceJD
includeSolarLongPeriod := jupiterGalileanUseSolarLongPeriod(jd)
return jupiterGalileanSatelliteStateAtET(et, satellite-1, includeSolarLongPeriod)
}
// JupiterGalileanSatelliteStates 四颗伽利略卫星木心 J2000 状态 / Jovicentric J2000 states of the four Galilean satellites.
//
// 返回次序固定为 Io、Europa、Ganymede、Callisto。
// The returned order is Io, Europa, Ganymede, Callisto.
func JupiterGalileanSatelliteStates(jd float64) [4]JupiterGalileanState {
var states [4]JupiterGalileanState
et := jd - jupiterGalileanReferenceJD
includeSolarLongPeriod := jupiterGalileanUseSolarLongPeriod(jd)
for i := range states {
states[i] = jupiterGalileanSatelliteStateAtET(et, i, includeSolarLongPeriod)
}
return states
}
// JupiterGalileanSatelliteObservation 伽利略卫星视位置 / apparent geometry of a Galilean satellite.
//
// jd 为 TT/TDB 对应儒略日;返回卫星的天球视赤道坐标,以及相对木星中心的东/北平面偏移。
// jd is a TT/TDB Julian day. The result contains the satellite's astrometric equatorial coordinates and its east/north sky-plane offsets relative to Jupiter's center.
func JupiterGalileanSatelliteObservation(jd float64, satellite int) JupiterGalileanObservation {
if satellite < 1 || satellite > 4 || !isFinite(jd) {
return invalidJupiterGalileanObservation()
}
context := newJupiterGalileanObservationContext(jd)
return context.observationForSatellite(satellite - 1)
}
// JupiterGalileanSatelliteObservations 四颗伽利略卫星视位置 / apparent geometry of the four Galilean satellites.
//
// 返回次序固定为 Io、Europa、Ganymede、Callisto。
// The returned order is Io, Europa, Ganymede, Callisto.
func JupiterGalileanSatelliteObservations(jd float64) [4]JupiterGalileanObservation {
var observations [4]JupiterGalileanObservation
context := newJupiterGalileanObservationContext(jd)
for i := range observations {
observations[i] = context.observationForSatellite(i)
}
return observations
}
type jupiterGalileanObservationContext struct {
jd float64
targetJD float64
earthHelioJ2000 Vector3
jupiterGeoJ2000 Vector3
jupiterDistance float64
jupiterLightTime float64
sunDistanceAU float64
east Vector3
north Vector3
lineOfSight Vector3
earthDirection Vector3
sunDirection Vector3
sunLineOfSight Vector3
sunEast Vector3
sunNorth Vector3
earthMinorRadius float64
sunMinorRadius float64
bodyX Vector3
bodyY Vector3
bodyZ Vector3
}
func newJupiterGalileanObservationContext(jd float64) jupiterGalileanObservationContext {
context := jupiterGalileanObservationContext{jd: jd}
if !isFinite(jd) {
return context
}
context.earthHelioJ2000 = rotateEclipticToEquatorial(earthHeliocentricVectorJ2000(jd), orbitJ2000Obliquity)
context.jupiterGeoJ2000, context.jupiterLightTime = jupiterAstrometricGeocentricVectorJ2000(jd, context.earthHelioJ2000)
context.targetJD = jd - context.jupiterLightTime
context.jupiterDistance = vectorMagnitude(context.jupiterGeoJ2000)
if context.jupiterDistance == 0 {
return context
}
context.lineOfSight = normalizeVector(context.jupiterGeoJ2000)
context.earthDirection = Vector3{-context.lineOfSight[0], -context.lineOfSight[1], -context.lineOfSight[2]}
ra, dec := vectorToRaDec(context.lineOfSight)
context.east = Vector3{-Sin(ra), Cos(ra), 0}
context.north = Vector3{-Cos(ra) * Sin(dec), -Sin(ra) * Sin(dec), Cos(dec)}
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(context.targetJD), orbitJ2000Obliquity)
context.sunDistanceAU = vectorMagnitude(jupiterHelio)
context.sunDirection = normalizeVector(Vector3{-jupiterHelio[0], -jupiterHelio[1], -jupiterHelio[2]})
context.sunLineOfSight = Vector3{-context.sunDirection[0], -context.sunDirection[1], -context.sunDirection[2]}
sunRA, sunDec := vectorToRaDec(context.sunLineOfSight)
context.sunEast = Vector3{-Sin(sunRA), Cos(sunRA), 0}
context.sunNorth = Vector3{-Cos(sunRA) * Sin(sunDec), -Sin(sunRA) * Sin(sunDec), Cos(sunDec)}
poleRA, poleDec, _ := jupiterPoleRotation(context.targetJD)
context.bodyZ = raDecToVector(poleRA, poleDec)
context.bodyX = normalizeVector(Vector3{-math.Sin(poleRA * rad), math.Cos(poleRA * rad), 0})
context.bodyY = normalizeVector(pxp(context.bodyZ, context.bodyX))
context.earthMinorRadius = jupiterProjectedMinorRadius(context.earthDirection, context.bodyZ)
context.sunMinorRadius = jupiterProjectedMinorRadius(context.sunDirection, context.bodyZ)
return context
}
func (context jupiterGalileanObservationContext) observationForSatellite(index int) JupiterGalileanObservation {
if index < 0 || index >= 4 || context.jupiterDistance == 0 {
return invalidJupiterGalileanObservation()
}
state, geocentric := jupiterGalileanSatelliteAstrometricGeocentric(index, context.jd, context.jupiterLightTime, context.earthHelioJ2000)
direction := normalizeVector(geocentric)
ra, dec := vectorToRaDec(direction)
distance := vectorMagnitude(geocentric)
relative := Vector3{
geocentric[0] - context.jupiterGeoJ2000[0],
geocentric[1] - context.jupiterGeoJ2000[1],
geocentric[2] - context.jupiterGeoJ2000[2],
}
zAU := vectorDot(relative, context.lineOfSight)
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
offsetXRad, offsetYRad := tangentPlaneOffsetAngles(direction, context.lineOfSight, context.east, context.north)
jupiterSemidiameterArcsec := math.Atan2(radiusAU, context.jupiterDistance) * deg * 3600
return JupiterGalileanObservation{
State: state,
RA: ra,
Dec: dec,
Distance: distance,
OffsetXArcsec: offsetXRad * deg * 3600,
OffsetYArcsec: offsetYRad * deg * 3600,
OffsetXJupiterRadii: offsetXRad * deg * 3600 / jupiterSemidiameterArcsec,
OffsetYJupiterRadii: offsetYRad * deg * 3600 / jupiterSemidiameterArcsec,
OffsetZJupiterRadii: zAU / radiusAU,
InFrontOfJupiter: zAU < 0,
}
}
func tangentPlaneOffsetAngles(target, center, east, north Vector3) (float64, float64) {
denominator := vectorDot(target, center)
return math.Atan2(vectorDot(target, east), denominator), math.Atan2(vectorDot(target, north), denominator)
}
func jupiterGalileanSatelliteAstrometricGeocentric(index int, jd, initialLightTime float64, earthHelioJ2000 Vector3) (JupiterGalileanState, Vector3) {
lightTime := initialLightTime
state := JupiterGalileanState{}
result := Vector3{}
includeSolarLongPeriod := jupiterGalileanUseSolarLongPeriod(jd)
for i := 0; i < 8; i++ {
targetJD := jd - lightTime
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(targetJD), orbitJ2000Obliquity)
state = jupiterGalileanSatelliteStateAtET(targetJD-jupiterGalileanReferenceJD, index, includeSolarLongPeriod)
result = Vector3{
jupiterHelio[0] + state.X - earthHelioJ2000[0],
jupiterHelio[1] + state.Y - earthHelioJ2000[1],
jupiterHelio[2] + state.Z - earthHelioJ2000[2],
}
nextLightTime := lightTimeDaysPerAU * vectorMagnitude(result)
if math.Abs(nextLightTime-lightTime) < 1e-12 {
break
}
lightTime = nextLightTime
}
return state, result
}
func jupiterAstrometricGeocentricVectorJ2000(jd float64, earthHelioJ2000 Vector3) (Vector3, float64) {
lightTime := 0.0
result := Vector3{}
for i := 0; i < 8; i++ {
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(jd-lightTime), orbitJ2000Obliquity)
result = Vector3{
jupiterHelio[0] - earthHelioJ2000[0],
jupiterHelio[1] - earthHelioJ2000[1],
jupiterHelio[2] - earthHelioJ2000[2],
}
nextLightTime := lightTimeDaysPerAU * vectorMagnitude(result)
if math.Abs(nextLightTime-lightTime) < 1e-12 {
return result, nextLightTime
}
lightTime = nextLightTime
}
return result, lightTime
}
func jupiterHeliocentricVectorJ2000(jd float64) Vector3 {
return eclipticVectorAtReferenceEpoch(
eclipticCartesian(
planet.WherePlanet(4, 0, jd),
planet.WherePlanet(4, 1, jd),
planet.WherePlanet(4, 2, jd),
),
jd,
orbitReferenceJD,
)
}
func jupiterGalileanSatelliteStateAtET(et float64, index int, includeSolarLongPeriod bool) JupiterGalileanState {
elements := jupiterGalileanElementsAtET(et, index, includeSolarLongPeriod)
pv := jupiterGalileanElementsToPV(jupiterGalileanMu[index], elements)
cosNode, sinNode := math.Cos(jupiterGalileanFrameNode), math.Sin(jupiterGalileanFrameNode)
cosTilt, sinTilt := math.Cos(jupiterGalileanFrameTilt), math.Sin(jupiterGalileanFrameTilt)
return JupiterGalileanState{
X: pv[0]*cosNode - pv[1]*sinNode*cosTilt + pv[2]*sinTilt*sinNode,
Y: pv[0]*sinNode + pv[1]*cosNode*cosTilt - pv[2]*sinTilt*cosNode,
Z: pv[1]*sinTilt + pv[2]*cosTilt,
VX: pv[3]*cosNode - pv[4]*sinNode*cosTilt + pv[5]*sinTilt*sinNode,
VY: pv[3]*sinNode + pv[4]*cosNode*cosTilt - pv[5]*sinTilt*cosNode,
VZ: pv[4]*sinTilt + pv[5]*cosTilt,
}
}
type jupiterGalileanElements struct {
A float64
L float64
K float64
H float64
Q float64
P float64
}
func jupiterGalileanElementsAtET(et float64, index int, includeSolarLongPeriod bool) jupiterGalileanElements {
longPeriod := jupiterGalileanEvaluateSeries(jupiterGalileanL1LongPeriodTerms[index], et+jupiterGalileanLongPeriodShift, et, includeSolarLongPeriod, index)
crossPeriod := jupiterGalileanEvaluateSeries(jupiterGalileanL1CrossPeriodTerms[index], et, et, false, index)
combined := jupiterGalileanElements{
A: longPeriod.A + crossPeriod.A,
L: longPeriod.L + crossPeriod.L + jupiterGalileanBaseMeanLongitudes[index]*et,
K: longPeriod.K + crossPeriod.K,
H: longPeriod.H + crossPeriod.H,
Q: longPeriod.Q + crossPeriod.Q,
P: longPeriod.P + crossPeriod.P,
}
combined.L = math.Atan2(math.Sin(combined.L), math.Cos(combined.L))
if combined.L < 0 {
combined.L += 2 * math.Pi
}
return combined
}
func jupiterGalileanEvaluateSeries(blocks [4][]jupiterGalileanL1Term, angleTime, et float64, includeSolarLongPeriod bool, index int) jupiterGalileanElements {
vals := [5]float64{}
if includeSolarLongPeriod {
x := (et/365.25 - 0.5*(812.721806990360-819.727638594856)) / (0.5 * (812.721806990360 - -819.727638594856))
tn := [9]float64{1, x}
for i := 2; i < len(tn); i++ {
tn[i] = 2*x*tn[i-1] - tn[i-2]
}
for variable := 0; variable < len(vals); variable++ {
sum := 0.0
for term := 0; term < len(tn); term++ {
sum += jupiterGalileanL1Chebyshev[index][variable][term] * tn[term]
}
vals[variable] = sum - 0.5*jupiterGalileanL1Chebyshev[index][variable][0]
}
}
result := jupiterGalileanElements{}
for blockIndex, terms := range blocks {
realPart, imagPart := 0.0, 0.0
for _, term := range terms {
angle := term.Phase
if term.Period != 0 {
angle += 2 * math.Pi * angleTime / term.Period
}
realPart += term.Amp * math.Cos(angle)
imagPart += term.Amp * math.Sin(angle)
}
switch blockIndex {
case 0:
result.A = realPart
case 1:
result.L = realPart + vals[0]
case 2:
result.K = realPart + vals[1]
result.H = imagPart + vals[2]
case 3:
result.Q = realPart + vals[3]
result.P = imagPart + vals[4]
}
}
return result
}
func jupiterGalileanElementsToPV(mu float64, elements jupiterGalileanElements) [6]float64 {
k := elements.K
h := elements.H
q := elements.Q
p := elements.P
a := elements.A
al := elements.L
an := math.Sqrt(mu / math.Pow(a, 3))
ee := al + k*math.Sin(al) - h*math.Cos(al)
for {
ce := math.Cos(ee)
se := math.Sin(ee)
de := (al - ee + k*se - h*ce) / (1 - k*ce - h*se)
ee += de
if math.Abs(de) < 1e-12 {
break
}
}
ce := math.Cos(ee)
se := math.Sin(ee)
dle := h*ce - k*se
rsam1 := -k*ce - h*se
asr := 1 / (1 + rsam1)
phi := math.Sqrt(1 - k*k - h*h)
psi := 1 / (1 + phi)
x1 := a * (ce - k - psi*h*dle)
y1 := a * (se - h + psi*k*dle)
vx1 := an * asr * a * (-se - psi*h*rsam1)
vy1 := an * asr * a * (ce + psi*k*rsam1)
f2 := 2 * math.Sqrt(1-q*q-p*p)
p2 := 1 - 2*p*p
q2 := 1 - 2*q*q
pq := 2 * p * q
return [6]float64{
x1*p2 + y1*pq,
x1*pq + y1*q2,
(q*y1 - x1*p) * f2,
vx1*p2 + vy1*pq,
vx1*pq + vy1*q2,
(q*vy1 - vx1*p) * f2,
}
}
func jupiterGalileanUseSolarLongPeriod(jd float64) bool {
year := 2000.0 + (jd-2451545.0)/365.25
return year >= jupiterGalileanMinSolarLPYear && year <= jupiterGalileanMaxSolarLPYear
}
func invalidJupiterGalileanState() JupiterGalileanState {
nan := math.NaN()
return JupiterGalileanState{X: nan, Y: nan, Z: nan, VX: nan, VY: nan, VZ: nan}
}
func invalidJupiterGalileanObservation() JupiterGalileanObservation {
nan := math.NaN()
return JupiterGalileanObservation{
State: invalidJupiterGalileanState(),
RA: nan,
Dec: nan,
Distance: nan,
OffsetXArcsec: nan,
OffsetYArcsec: nan,
OffsetXJupiterRadii: nan,
OffsetYJupiterRadii: nan,
OffsetZJupiterRadii: nan,
InFrontOfJupiter: false,
}
}
+75
View File
@@ -0,0 +1,75 @@
package basic
import (
"math"
"testing"
)
const galileanSampleToleranceAU = 1e-15
const galileanSampleToleranceAUDay = 1e-15
type galileanSample struct {
name string
index int
state JupiterGalileanState
}
func TestJupiterGalileanSatelliteStateMatchesIMCCESample(t *testing.T) {
samples := []galileanSample{
{
name: "Io",
index: 1,
state: JupiterGalileanState{X: 2.671999370920431e-003, Y: 7.644018403387422e-004, Z: 4.087344808808269e-004, VX: -3.116203340625001e-003, VY: 8.645679572984422e-003, VZ: 4.066210333795641e-003},
},
{
name: "Europa",
index: 2,
state: JupiterGalileanState{X: -3.751373844521062e-003, Y: -2.136179970327756e-003, Z: -1.056765216826830e-003, VX: 4.310591732986133e-003, VY: -6.143199976514738e-003, VZ: -2.800434328620005e-003},
},
{
name: "Ganymede",
index: 3,
state: JupiterGalileanState{X: -5.490036250442612e-003, Y: -4.112229247907583e-003, Z: -2.033821277493470e-003, VX: 4.036147912130572e-003, VY: -4.364866691392988e-003, VZ: -2.037111499364415e-003},
},
{
name: "Callisto",
index: 4,
state: JupiterGalileanState{X: 2.172082907229073e-003, Y: 1.118792302205555e-002, Z: 5.322275059416266e-003, VX: -4.662583658656747e-003, VY: 7.976685330152526e-004, VZ: 3.092058747362411e-004},
},
}
const jd = 2451545.0
maxPosDiff := 0.0
maxVelDiff := 0.0
for _, sample := range samples {
got := JupiterGalileanSatelliteState(jd, sample.index)
posDiffs := []float64{
math.Abs(got.X - sample.state.X),
math.Abs(got.Y - sample.state.Y),
math.Abs(got.Z - sample.state.Z),
}
velDiffs := []float64{
math.Abs(got.VX - sample.state.VX),
math.Abs(got.VY - sample.state.VY),
math.Abs(got.VZ - sample.state.VZ),
}
for i, diff := range posDiffs {
if diff > maxPosDiff {
maxPosDiff = diff
}
if diff > galileanSampleToleranceAU {
t.Fatalf("%s position[%d] mismatch: got %.18e want %.18e", sample.name, i, []float64{got.X, got.Y, got.Z}[i], []float64{sample.state.X, sample.state.Y, sample.state.Z}[i])
}
}
for i, diff := range velDiffs {
if diff > maxVelDiff {
maxVelDiff = diff
}
if diff > galileanSampleToleranceAUDay {
t.Fatalf("%s velocity[%d] mismatch: got %.18e want %.18e", sample.name, i, []float64{got.VX, got.VY, got.VZ}[i], []float64{sample.state.VX, sample.state.VY, sample.state.VZ}[i])
}
}
}
// Official IMCCE README example for V1_1 at JD 2451545.0.
t.Logf("galilean IMCCE sample max diff: position=%.3e AU velocity=%.3e AU/day", maxPosDiff, maxVelDiff)
}
-13
View File
@@ -1,13 +0,0 @@
package basic
import (
"fmt"
"testing"
)
func TestJupiter(t *testing.T) {
jde := GetNowJDE() - 6000
for i := 0.00; i < 20; i++ {
fmt.Println(jde+i*365, JDE2Date(jde+i*365), JDE2Date(NextJupiterRetrogradeToPrograde(jde+i*365)))
}
}
+347
View File
@@ -0,0 +1,347 @@
package basic
import "math"
// LunarEclipseType 表示月食类型。
type LunarEclipseType string
const (
// LunarEclipseNone 表示该次望月没有发生月食。
LunarEclipseNone LunarEclipseType = "none"
// LunarEclipsePenumbral 表示半影月食。
LunarEclipsePenumbral LunarEclipseType = "penumbral"
// LunarEclipsePartial 表示月偏食。
LunarEclipsePartial LunarEclipseType = "partial"
// LunarEclipseTotal 表示月全食。
LunarEclipseTotal LunarEclipseType = "total"
)
// LunarEclipseResult 表示一次望月附近的月食几何结果。
//
// 所有时刻字段都使用力学时儒略日(JDE, TT)。
// 输入 seedJDE 只需要落在目标望月附近,允许相差数天。
type LunarEclipseResult struct {
Type LunarEclipseType
// Maximum 是食甚时刻;即使最终没有月食,也会返回该次望月附近
// “月面中心最接近地影中心”的几何极值时刻。
Maximum float64
// Magnitude 是本影食分。纯半影月食时可为负值;无月食时为 0。
Magnitude float64
// PenumbralMagnitude 是半影食分。无半影接触时为 0。
PenumbralMagnitude float64
// MinimumDistance 是食甚时月心到地影中心的最小角距离,单位为弧度。
MinimumDistance float64
// Contact times:
// PenumbralStart / PenumbralEnd: 半影食始 / 半影食终
// PartialStart / PartialEnd: 初亏 / 复圆
// TotalStart / TotalEnd: 食既 / 生光
PenumbralStart float64
PenumbralEnd float64
PartialStart float64
PartialEnd float64
TotalStart float64
TotalEnd float64
HasPenumbral bool
HasPartial bool
HasTotal bool
}
type lunarShadowState struct {
jde float64
x float64
y float64
moonRadiusRad float64
umbraRadiusRad float64
penumbraRadiusRad float64
}
type lunarEclipseShadowModel int
const (
lunarEclipseShadowDanjon lunarEclipseShadowModel = iota
lunarEclipseShadowChauvenet
)
const (
lunarEarthEquatorialRadiusKM = 6378.1366
lunarAstronomicalUnitKM = 1.49597870691e8
// 沿用月食常量:
// - 0.2725076 用于月亮视半径和半影几何
// - 959.63 / 8.794 分别为太阳视半径与太阳视差的常用角秒常量
lunarMoonRadiusRatio = 0.2725076
lunarMoonRadiusScale = lunarMoonRadiusRatio * lunarEarthEquatorialRadiusKM * 1.0000036
lunarSolarRadiusArcsec = 959.63
lunarSolarParallaxArcsec = 8.794
lunarLongitudeAberration = -3.4e-6
lunarFiniteDifferenceStep = 60.0 / 86400.0
// Chauvenet 体系:
// - 地球有效半径取 0.99834 * 赤道半径
// - 再统一乘 51/50 的大气放大因子
lunarChauvenetEarthScale = 0.99834
lunarChauvenetShadowGain = 51.0 / 50.0
// Danjon 体系:
// - 影半径只对月球水平视差项乘 1.01
// - 太阳视半径与太阳视差项不再统一乘 1.02
lunarDanjonParallaxScale = 1.01
)
var lunarArcsecPerRadian = 180.0 * 3600.0 / math.Pi
// LunarEclipse 计算给定近望时刻附近的一次月食,默认使用 Danjon 影半径模型。
//
// seedJDE 为力学时儒略日(TT),只需落在目标望月附近,允许相差数天。
// 返回值中的所有接触时刻也都是力学时儒略日。
func LunarEclipse(seedJDE float64) LunarEclipseResult {
return LunarEclipseDanjon(seedJDE)
}
// LunarEclipseDanjon 计算给定近望时刻附近的一次月食,使用 Danjon 影半径模型。
func LunarEclipseDanjon(seedJDE float64) LunarEclipseResult {
return lunarEclipse(seedJDE, lunarEclipseShadowDanjon)
}
// LunarEclipseChauvenet 计算给定近望时刻附近的一次月食,使用 Chauvenet 影半径模型。
func LunarEclipseChauvenet(seedJDE float64) LunarEclipseResult {
return lunarEclipse(seedJDE, lunarEclipseShadowChauvenet)
}
func lunarEclipse(seedJDE float64, shadowModel lunarEclipseShadowModel) LunarEclipseResult {
fullMoonJDE := CalcMoonSHByJDE(seedJDE, 1)
maximumJDE, state, dxdt, dydt, minimumDistance := refineLunarEclipseMaximum(fullMoonJDE, shadowModel)
result := LunarEclipseResult{
Type: LunarEclipseNone,
Maximum: maximumJDE,
MinimumDistance: minimumDistance,
PenumbralMagnitude: (state.moonRadiusRad + state.penumbraRadiusRad - minimumDistance) / (2 * state.moonRadiusRad),
}
rawUmbralMagnitude := (state.moonRadiusRad + state.umbraRadiusRad - minimumDistance) / (2 * state.moonRadiusRad)
if result.PenumbralMagnitude < 0 {
result.PenumbralMagnitude = 0
}
if minimumDistance <= state.moonRadiusRad+state.penumbraRadiusRad {
result.Type = LunarEclipsePenumbral
result.HasPenumbral = true
result.Magnitude = rawUmbralMagnitude
result.PenumbralStart = refineLunarEclipseContact(
state, dxdt, dydt, state.moonRadiusRad+state.penumbraRadiusRad, false, shadowModel,
)
result.PenumbralEnd = refineLunarEclipseContact(
state, dxdt, dydt, state.moonRadiusRad+state.penumbraRadiusRad, true, shadowModel,
)
}
if minimumDistance <= state.moonRadiusRad+state.umbraRadiusRad {
result.Type = LunarEclipsePartial
result.HasPartial = true
result.Magnitude = rawUmbralMagnitude
result.PartialStart = refineLunarEclipseContact(
state, dxdt, dydt, state.moonRadiusRad+state.umbraRadiusRad, false, shadowModel,
)
result.PartialEnd = refineLunarEclipseContact(
state, dxdt, dydt, state.moonRadiusRad+state.umbraRadiusRad, true, shadowModel,
)
}
if minimumDistance <= state.umbraRadiusRad-state.moonRadiusRad {
result.Type = LunarEclipseTotal
result.HasTotal = true
result.TotalStart = refineLunarEclipseContact(
state, dxdt, dydt, state.umbraRadiusRad-state.moonRadiusRad, false, shadowModel,
)
result.TotalEnd = refineLunarEclipseContact(
state, dxdt, dydt, state.umbraRadiusRad-state.moonRadiusRad, true, shadowModel,
)
}
return result
}
// refineLunarEclipseMaximum 从近望初值出发,用有限差分速度做两轮几何极值修正。
//
// 这里直接在月心相对地影中心的二维平面上求 |r| 的极小值,
func refineLunarEclipseMaximum(
seedJDE float64,
shadowModel lunarEclipseShadowModel,
) (float64, lunarShadowState, float64, float64, float64) {
currentJDE := seedJDE
for i := 0; i < 2; i++ {
state := computeLunarShadowState(currentJDE, shadowModel)
nextState := computeLunarShadowState(currentJDE+lunarFiniteDifferenceStep, shadowModel)
dxdt := (nextState.x - state.x) / lunarFiniteDifferenceStep
dydt := (nextState.y - state.y) / lunarFiniteDifferenceStep
denominator := dxdt*dxdt + dydt*dydt
if denominator == 0 {
finalState := computeLunarShadowState(currentJDE, shadowModel)
return currentJDE, finalState, 0, 0, math.Hypot(finalState.x, finalState.y)
}
correction := -(state.x*dxdt + state.y*dydt) / denominator
currentJDE += correction
}
linearState := computeLunarShadowState(currentJDE, shadowModel)
nextLinearState := computeLunarShadowState(currentJDE+lunarFiniteDifferenceStep, shadowModel)
dxdt := (nextLinearState.x - linearState.x) / lunarFiniteDifferenceStep
dydt := (nextLinearState.y - linearState.y) / lunarFiniteDifferenceStep
denominator := dxdt*dxdt + dydt*dydt
if denominator == 0 {
return currentJDE, linearState, dxdt, dydt, math.Hypot(linearState.x, linearState.y)
}
correction := -(linearState.x*dxdt + linearState.y*dydt) / denominator
maximumJDE := currentJDE + correction
finalState := computeLunarShadowState(maximumJDE, shadowModel)
nextState := computeLunarShadowState(maximumJDE+lunarFiniteDifferenceStep, shadowModel)
dxdt = (nextState.x - finalState.x) / lunarFiniteDifferenceStep
dydt = (nextState.y - finalState.y) / lunarFiniteDifferenceStep
return maximumJDE, finalState, dxdt, dydt, math.Hypot(finalState.x, finalState.y)
}
// refineLunarEclipseContact 先用固定速度近似求一次接触时刻,
// 再在接触点重算半径并修正一次。
func refineLunarEclipseContact(
maximumState lunarShadowState,
dxdt, dydt, boundaryRadius float64,
afterMaximum bool,
shadowModel lunarEclipseShadowModel,
) float64 {
firstGuess, ok := solveLineCircleContact(maximumState, dxdt, dydt, boundaryRadius, afterMaximum)
if !ok {
return 0
}
contactState := computeLunarShadowState(firstGuess, shadowModel)
refinedRadius := boundaryRadius
switch {
case math.Abs(boundaryRadius-(maximumState.moonRadiusRad+maximumState.umbraRadiusRad)) < 1e-18:
refinedRadius = contactState.moonRadiusRad + contactState.umbraRadiusRad
case math.Abs(boundaryRadius-(maximumState.moonRadiusRad+maximumState.penumbraRadiusRad)) < 1e-18:
refinedRadius = contactState.moonRadiusRad + contactState.penumbraRadiusRad
case math.Abs(boundaryRadius-(maximumState.umbraRadiusRad-maximumState.moonRadiusRad)) < 1e-18:
refinedRadius = contactState.umbraRadiusRad - contactState.moonRadiusRad
}
refinedGuess, ok := solveLineCircleContact(contactState, dxdt, dydt, refinedRadius, afterMaximum)
if !ok {
return firstGuess
}
return refinedGuess
}
// solveLineCircleContact 求月心轨迹与某个影界圆的交点时刻。
func solveLineCircleContact(
state lunarShadowState,
dxdt, dydt, radius float64,
afterMaximum bool,
) (float64, bool) {
a := dxdt*dxdt + dydt*dydt
if a == 0 {
return 0, false
}
b := 2 * (state.x*dxdt + state.y*dydt)
c := state.x*state.x + state.y*state.y - radius*radius
discriminant := b*b - 4*a*c
if discriminant < 0 {
return 0, false
}
root := math.Sqrt(discriminant)
delta := (-b - root) / (2 * a)
if afterMaximum {
delta = (-b + root) / (2 * a)
}
return state.jde + delta, true
}
// computeLunarShadowState 计算某一力学时刻下,月心相对地影中心的二维几何状态。
//
// 所有内部角量统一使用弧度。影半径模型允许在 Danjon 与 Chauvenet 之间切换,
// 其余月心轨迹与几何求交框架保持一致。
func computeLunarShadowState(jde float64, shadowModel lunarEclipseShadowModel) lunarShadowState {
julianCentury := (jde - 2451545.0) / 36525.0
sunLongitude := HSunTrueLo(jde)*rad + sunLongitudeAberrationRad(julianCentury)
sunLatitude := HSunTrueBo(jde) * rad
moonLongitude := HMoonTrueLo(jde)*rad + lunarLongitudeAberration
moonLatitude := HMoonTrueBo(jde)*rad + moonLatitudeAberrationRad(julianCentury)
moonDistanceKM := HMoonAway(jde)
sunDistanceAU := EarthAway(jde)
moonRadiusArcsec := lunarMoonRadiusScale * lunarArcsecPerRadian / moonDistanceKM
earthParallaxArcsec := lunarEarthEquatorialRadiusKM / moonDistanceKM * lunarArcsecPerRadian
solarRadiusArcsec := lunarSolarRadiusArcsec / sunDistanceAU
solarParallaxArcsec := lunarSolarParallaxArcsec / sunDistanceAU
umbraRadiusArcsec, penumbraRadiusArcsec := lunarEclipseShadowRadiiArcsec(
earthParallaxArcsec,
solarRadiusArcsec,
solarParallaxArcsec,
shadowModel,
)
return lunarShadowState{
jde: jde,
x: normalizeRadians(moonLongitude+math.Pi-sunLongitude) * math.Cos((moonLatitude-sunLatitude)/2),
y: moonLatitude + sunLatitude,
moonRadiusRad: moonRadiusArcsec / lunarArcsecPerRadian,
umbraRadiusRad: umbraRadiusArcsec / lunarArcsecPerRadian,
penumbraRadiusRad: penumbraRadiusArcsec / lunarArcsecPerRadian,
}
}
func lunarEclipseShadowRadiiArcsec(
earthParallaxArcsec, solarRadiusArcsec, solarParallaxArcsec float64,
shadowModel lunarEclipseShadowModel,
) (float64, float64) {
switch shadowModel {
case lunarEclipseShadowDanjon:
earthTerm := lunarDanjonParallaxScale * earthParallaxArcsec
return earthTerm - solarRadiusArcsec + solarParallaxArcsec,
earthTerm + solarRadiusArcsec + solarParallaxArcsec
default:
earthTerm := lunarChauvenetEarthScale * earthParallaxArcsec
return (earthTerm - solarRadiusArcsec + solarParallaxArcsec) * lunarChauvenetShadowGain,
(earthTerm + solarRadiusArcsec + solarParallaxArcsec) * lunarChauvenetShadowGain
}
}
func normalizeRadians(angle float64) float64 {
angle = math.Mod(angle, 2*math.Pi)
if angle > math.Pi {
angle -= 2 * math.Pi
}
if angle <= -math.Pi {
angle += 2 * math.Pi
}
return angle
}
func sunLongitudeAberrationRad(julianCentury float64) float64 {
meanAnomaly := -0.043126 + 628.301955*julianCentury - 0.000002732*julianCentury*julianCentury
eccentricity := 0.016708634 - 0.000042037*julianCentury - 0.0000001267*julianCentury*julianCentury
return -20.49552 * (1 + eccentricity*math.Cos(meanAnomaly)) / lunarArcsecPerRadian
}
func moonLatitudeAberrationRad(julianCentury float64) float64 {
argument := 0.057 + 8433.4662*julianCentury + 0.000064*julianCentury*julianCentury
return 0.063 * math.Sin(argument) / lunarArcsecPerRadian
}
+253
View File
@@ -0,0 +1,253 @@
package basic
import (
"math"
"sort"
)
const (
lunarEclipseDiagramDefaultStepDays = 5.0 / 1440.0
lunarEclipseDiagramMinStepDays = 1.0 / 86400.0
lunarEclipseDiagramMaxSamples = 2000
lunarEclipseDiagramDuplicateDays = 1e-10
)
// LunarEclipseDiagramOptions 控制月食穿影图采样。
// LunarEclipseDiagramOptions controls lunar eclipse shadow-path diagram sampling.
type LunarEclipseDiagramOptions struct {
// StepDays 是路径采样步长,单位为日;<=0 时使用 5 分钟。
// StepDays is the path sampling step in days; values <= 0 use five minutes.
StepDays float64
}
// LunarEclipseDiagramPoint 表示月食穿影图上的一个月心位置。
// LunarEclipseDiagramPoint is one Moon-center point in a lunar eclipse diagram.
type LunarEclipseDiagramPoint struct {
// JDE 是力学时儒略日, TT Julian ephemeris day.
JDE float64
// X / Y 是以月球半径为单位的月心相对地影中心坐标。
// X/Y are Moon-center coordinates relative to the shadow center, in Moon-radius units.
X float64
Y float64
// Label 是关键接触标签,如 P1/U1/U2/Greatest/U3/U4/P4。
// Label is a key contact label such as P1/U1/U2/Greatest/U3/U4/P4.
Label string
// Labels 是该点对应的全部关键接触标签;若事件重合,这里会有多个值。
// Labels contains all contact labels attached to this point.
Labels []string
}
// LunarEclipseDiagramResult 表示月食穿影图几何结果。
// LunarEclipseDiagramResult is the geometry result for a lunar eclipse diagram.
type LunarEclipseDiagramResult struct {
// Eclipse 是对应的月食结果。
// Eclipse is the eclipse result used for the diagram.
Eclipse LunarEclipseResult
// MoonRadius 是月球半径,单位为图上月球半径;固定为 1。
// MoonRadius is the Moon radius in diagram Moon-radius units; always 1.
MoonRadius float64
// UmbraRadius 是本影半径,单位为图上月球半径。
// UmbraRadius is the umbral shadow radius in Moon-radius units.
UmbraRadius float64
// PenumbraRadius 是半影半径,单位为图上月球半径。
// PenumbraRadius is the penumbral shadow radius in Moon-radius units.
PenumbraRadius float64
// Points 是月心路径点,包含接触点与采样点。
// Points are Moon-center path points, including contact and sampled points.
Points []LunarEclipseDiagramPoint
// StepDays 是实际采用的路径采样步长,单位为日。
// StepDays is the effective path sampling step in days.
StepDays float64
}
type lunarEclipseDiagramTime struct {
jde float64
labels []string
}
// LunarEclipseDiagram 计算月食穿影图几何数据,默认使用 Danjon 影半径模型。
// LunarEclipseDiagram computes lunar eclipse diagram geometry, using the Danjon shadow model by default.
func LunarEclipseDiagram(seedJDE float64, options LunarEclipseDiagramOptions) LunarEclipseDiagramResult {
return LunarEclipseDiagramDanjon(seedJDE, options)
}
// LunarEclipseDiagramDanjon 计算月食穿影图几何数据,使用 Danjon 影半径模型。
// LunarEclipseDiagramDanjon computes lunar eclipse diagram geometry with the Danjon shadow model.
func LunarEclipseDiagramDanjon(seedJDE float64, options LunarEclipseDiagramOptions) LunarEclipseDiagramResult {
return lunarEclipseDiagram(seedJDE, lunarEclipseShadowDanjon, options)
}
// LunarEclipseDiagramChauvenet 计算月食穿影图几何数据,使用 Chauvenet 影半径模型。
// LunarEclipseDiagramChauvenet computes lunar eclipse diagram geometry with the Chauvenet shadow model.
func LunarEclipseDiagramChauvenet(seedJDE float64, options LunarEclipseDiagramOptions) LunarEclipseDiagramResult {
return lunarEclipseDiagram(seedJDE, lunarEclipseShadowChauvenet, options)
}
func lunarEclipseDiagram(
seedJDE float64,
shadowModel lunarEclipseShadowModel,
options LunarEclipseDiagramOptions,
) LunarEclipseDiagramResult {
options = normalizeLunarEclipseDiagramOptions(options)
eclipse := lunarEclipse(seedJDE, shadowModel)
result := LunarEclipseDiagramResult{
Eclipse: eclipse,
StepDays: options.StepDays,
}
if !eclipse.HasPenumbral {
return result
}
maximumState := computeLunarShadowState(eclipse.Maximum, shadowModel)
if maximumState.moonRadiusRad <= 0 {
return result
}
result.MoonRadius = 1
result.UmbraRadius = maximumState.umbraRadiusRad / maximumState.moonRadiusRad
result.PenumbraRadius = maximumState.penumbraRadiusRad / maximumState.moonRadiusRad
times, stepDays := lunarEclipseDiagramTimes(eclipse, options.StepDays)
result.StepDays = stepDays
result.Points = make([]LunarEclipseDiagramPoint, 0, len(times))
for _, item := range times {
state := computeLunarShadowState(item.jde, shadowModel)
result.Points = append(result.Points, LunarEclipseDiagramPoint{
JDE: item.jde,
X: state.x / maximumState.moonRadiusRad,
Y: state.y / maximumState.moonRadiusRad,
Label: lunarEclipseDiagramPrimaryLabel(item.labels),
Labels: append([]string(nil), item.labels...),
})
}
return result
}
func normalizeLunarEclipseDiagramOptions(options LunarEclipseDiagramOptions) LunarEclipseDiagramOptions {
if options.StepDays <= 0 || math.IsNaN(options.StepDays) || math.IsInf(options.StepDays, 0) {
options.StepDays = lunarEclipseDiagramDefaultStepDays
}
if options.StepDays < lunarEclipseDiagramMinStepDays {
options.StepDays = lunarEclipseDiagramMinStepDays
}
return options
}
func lunarEclipseDiagramTimes(eclipse LunarEclipseResult, stepDays float64) ([]lunarEclipseDiagramTime, float64) {
startJDE := eclipse.PenumbralStart
endJDE := eclipse.PenumbralEnd
if startJDE == 0 || endJDE == 0 || endJDE <= startJDE {
return nil, stepDays
}
if sampleCount := int(math.Ceil((endJDE-startJDE)/stepDays)) + 1; sampleCount > lunarEclipseDiagramMaxSamples {
stepDays = (endJDE - startJDE) / float64(lunarEclipseDiagramMaxSamples-1)
}
times := []lunarEclipseDiagramTime{
{jde: startJDE, labels: []string{"P1"}},
{jde: eclipse.Maximum, labels: []string{"Greatest"}},
{jde: endJDE, labels: []string{"P4"}},
}
if eclipse.HasPartial {
times = append(times,
lunarEclipseDiagramTime{jde: eclipse.PartialStart, labels: []string{"U1"}},
lunarEclipseDiagramTime{jde: eclipse.PartialEnd, labels: []string{"U4"}},
)
}
if eclipse.HasTotal {
times = append(times,
lunarEclipseDiagramTime{jde: eclipse.TotalStart, labels: []string{"U2"}},
lunarEclipseDiagramTime{jde: eclipse.TotalEnd, labels: []string{"U3"}},
)
}
for jde := startJDE + stepDays; jde < endJDE; jde += stepDays {
times = append(times, lunarEclipseDiagramTime{jde: jde})
}
sort.SliceStable(times, func(i, j int) bool {
if times[i].jde == times[j].jde {
return lunarEclipseDiagramLabelPriority(times[i].labels) < lunarEclipseDiagramLabelPriority(times[j].labels)
}
return times[i].jde < times[j].jde
})
return uniqueLunarEclipseDiagramTimes(times), stepDays
}
func uniqueLunarEclipseDiagramTimes(times []lunarEclipseDiagramTime) []lunarEclipseDiagramTime {
if len(times) < 2 {
return times
}
unique := times[:0]
for _, item := range times {
if item.jde == 0 {
continue
}
if len(unique) == 0 || math.Abs(item.jde-unique[len(unique)-1].jde) > lunarEclipseDiagramDuplicateDays {
item.labels = append([]string(nil), item.labels...)
unique = append(unique, item)
continue
}
unique[len(unique)-1].labels = mergeLunarEclipseDiagramLabels(unique[len(unique)-1].labels, item.labels)
}
return unique
}
func mergeLunarEclipseDiagramLabels(existing, incoming []string) []string {
if len(incoming) == 0 {
return existing
}
if len(existing) == 0 {
return append([]string(nil), incoming...)
}
for _, label := range incoming {
found := false
for _, current := range existing {
if current == label {
found = true
break
}
}
if !found {
existing = append(existing, label)
}
}
return existing
}
func lunarEclipseDiagramPrimaryLabel(labels []string) string {
for _, label := range labels {
if label == "Greatest" {
return label
}
}
if len(labels) == 0 {
return ""
}
return labels[0]
}
func lunarEclipseDiagramLabelPriority(labels []string) int {
if len(labels) == 0 {
return 99
}
switch labels[0] {
case "P1":
return 0
case "U1":
return 1
case "U2":
return 2
case "Greatest":
return 3
case "U3":
return 4
case "U4":
return 5
case "P4":
return 6
default:
return 99
}
}
+308
View File
@@ -0,0 +1,308 @@
package basic
import (
"math"
"testing"
)
type lunarEclipseBaseline struct {
name string
jde float64
expectedType LunarEclipseType
expectedMax float64
expectedMag float64
expectedPenumbralStart float64
expectedPenumbralEnd float64
expectedPartialStart float64
expectedPartialEnd float64
expectedTotalStart float64
expectedTotalEnd float64
}
func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
// 这些基准值来自历史本地月食基线,
// 其阴影口径对应当前保留的 Chauvenet 模型。
testCases := []lunarEclipseBaseline{
{
name: "2022-11-08 total",
jde: JDECalc(2022, 11, 8),
expectedType: LunarEclipseTotal,
expectedMax: 2459891.9585873615,
expectedMag: 1.3635170051692678,
expectedPenumbralStart: 2459891.8346063416,
expectedPenumbralEnd: 2459892.0826413140,
expectedPartialStart: 2459891.8820205650,
expectedPartialEnd: 2459892.0351211606,
expectedTotalStart: 2459891.9288277230,
expectedTotalEnd: 2459891.9883249460,
},
{
name: "2023-05-05 penumbral",
jde: JDECalc(2023, 5, 5),
expectedType: LunarEclipsePenumbral,
expectedPenumbralStart: 2460070.1342392800,
expectedPenumbralEnd: 2460070.3159191823,
},
{
name: "2023-10-28 partial",
jde: JDECalc(2023, 10, 28),
expectedType: LunarEclipsePartial,
expectedMax: 2460246.3439460830,
expectedMag: 0.12723850274626405,
expectedPenumbralStart: 2460246.2507697106,
expectedPenumbralEnd: 2460246.4370874465,
expectedPartialStart: 2460246.3164327650,
expectedPartialEnd: 2460246.3713359070,
},
{
name: "2024-03-25 penumbral",
jde: JDECalc(2024, 3, 25),
expectedType: LunarEclipsePenumbral,
expectedPenumbralStart: 2460394.7028870000,
expectedPenumbralEnd: 2460394.8999071894,
},
{
name: "2024-09-18 partial",
jde: JDECalc(2024, 9, 18),
expectedType: LunarEclipsePartial,
expectedMax: 2460571.6148748010,
expectedMag: 0.09042791952817894,
expectedPenumbralStart: 2460571.5281155687,
expectedPenumbralEnd: 2460571.7016473800,
expectedPartialStart: 2460571.5923644140,
expectedPartialEnd: 2460571.6374154520,
},
{
name: "2025-03-14 total",
jde: JDECalc(2025, 3, 14),
expectedType: LunarEclipseTotal,
expectedMax: 2460748.7916214615,
expectedMag: 1.1828107517800281,
expectedPenumbralStart: 2460748.6645233813,
expectedPenumbralEnd: 2460748.9187600957,
expectedPartialStart: 2460748.7156107454,
expectedPartialEnd: 2460748.8676076555,
expectedTotalStart: 2460748.7685903380,
expectedTotalEnd: 2460748.8146345600,
},
{
name: "2025-09-07 total",
jde: JDECalc(2025, 9, 7),
expectedType: LunarEclipseTotal,
expectedMax: 2460926.2590034613,
expectedMag: 1.3672329695760280,
expectedPenumbralStart: 2460926.1445036167,
expectedPenumbralEnd: 2460926.3734498024,
expectedPartialStart: 2460926.1860739910,
expectedPartialEnd: 2460926.3319619163,
expectedTotalStart: 2460926.2302397094,
expectedTotalEnd: 2460926.2877871464,
},
{
name: "2026-03-03 total",
jde: JDECalc(2026, 3, 3),
expectedType: LunarEclipseTotal,
expectedMax: 2461102.9825476190,
expectedMag: 1.1556387222746651,
expectedPenumbralStart: 2461102.8638708987,
expectedPenumbralEnd: 2461103.1012840020,
expectedPartialStart: 2461102.9103626400,
expectedPartialEnd: 2461103.0546894810,
expectedTotalStart: 2461102.9619182530,
expectedTotalEnd: 2461103.0031447060,
},
}
const timeTolerance = 1e-6
const magnitudeTolerance = 2e-5
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := LunarEclipseChauvenet(tc.jde)
if result.Type != tc.expectedType {
t.Fatalf("Type mismatch: got %s want %s", result.Type, tc.expectedType)
}
if tc.expectedMax != 0 && math.Abs(result.Maximum-tc.expectedMax) > timeTolerance {
t.Fatalf("Maximum mismatch: got %.12f want %.12f", result.Maximum, tc.expectedMax)
}
if tc.expectedMag != 0 && math.Abs(result.Magnitude-tc.expectedMag) > magnitudeTolerance {
t.Fatalf("Magnitude mismatch: got %.12f want %.12f", result.Magnitude, tc.expectedMag)
}
assertCloseJD(t, "PenumbralStart", result.PenumbralStart, tc.expectedPenumbralStart, timeTolerance)
assertCloseJD(t, "PenumbralEnd", result.PenumbralEnd, tc.expectedPenumbralEnd, timeTolerance)
assertCloseJD(t, "PartialStart", result.PartialStart, tc.expectedPartialStart, timeTolerance)
assertCloseJD(t, "PartialEnd", result.PartialEnd, tc.expectedPartialEnd, timeTolerance)
assertCloseJD(t, "TotalStart", result.TotalStart, tc.expectedTotalStart, timeTolerance)
assertCloseJD(t, "TotalEnd", result.TotalEnd, tc.expectedTotalEnd, timeTolerance)
if result.HasTotal && !(result.TotalStart < result.Maximum && result.Maximum < result.TotalEnd) {
t.Fatalf("total contact order invalid: start=%.12f max=%.12f end=%.12f", result.TotalStart, result.Maximum, result.TotalEnd)
}
if result.HasPartial && !(result.PartialStart < result.Maximum && result.Maximum < result.PartialEnd) {
t.Fatalf("partial contact order invalid: start=%.12f max=%.12f end=%.12f", result.PartialStart, result.Maximum, result.PartialEnd)
}
if result.HasPenumbral && !(result.PenumbralStart < result.Maximum && result.Maximum < result.PenumbralEnd) {
t.Fatalf("penumbral contact order invalid: start=%.12f max=%.12f end=%.12f", result.PenumbralStart, result.Maximum, result.PenumbralEnd)
}
})
}
}
func TestLunarEclipseDefaultUsesDanjon(t *testing.T) {
jde := JDECalc(2025, 3, 14)
defaultResult := LunarEclipse(jde)
danjonResult := LunarEclipseDanjon(jde)
chauvenetResult := LunarEclipseChauvenet(jde)
assertCloseJD(t, "Maximum", defaultResult.Maximum, danjonResult.Maximum, 1e-12)
assertCloseJD(t, "PenumbralStart", defaultResult.PenumbralStart, danjonResult.PenumbralStart, 1e-12)
assertCloseJD(t, "PenumbralEnd", defaultResult.PenumbralEnd, danjonResult.PenumbralEnd, 1e-12)
if math.Abs(defaultResult.PenumbralMagnitude-danjonResult.PenumbralMagnitude) > 1e-12 {
t.Fatalf("default penumbral magnitude mismatch: got %.12f want %.12f", defaultResult.PenumbralMagnitude, danjonResult.PenumbralMagnitude)
}
if math.Abs(defaultResult.PenumbralMagnitude-chauvenetResult.PenumbralMagnitude) < 1e-4 {
t.Fatalf("default model should not collapse to Chauvenet: default=%.12f chauvenet=%.12f", defaultResult.PenumbralMagnitude, chauvenetResult.PenumbralMagnitude)
}
}
func TestPenumbralLunarEclipseKeepsNegativeUmbralMagnitude(t *testing.T) {
testCases := []struct {
name string
jde float64
calc func(float64) LunarEclipseResult
}{
{name: "default 2024-03-25", jde: JDECalc(2024, 3, 25), calc: LunarEclipse},
{name: "danjon 2024-03-25", jde: JDECalc(2024, 3, 25), calc: LunarEclipseDanjon},
{name: "chauvenet 2023-05-05", jde: JDECalc(2023, 5, 5), calc: LunarEclipseChauvenet},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := tc.calc(tc.jde)
if result.Type != LunarEclipsePenumbral {
t.Fatalf("type mismatch: got %s want %s", result.Type, LunarEclipsePenumbral)
}
if !result.HasPenumbral || result.HasPartial || result.HasTotal {
t.Fatalf("unexpected eclipse flags: %+v", result)
}
if !(result.Magnitude < 0) {
t.Fatalf("expected negative umbral magnitude for penumbral eclipse, got %.12f", result.Magnitude)
}
if !(result.PenumbralMagnitude > 0) {
t.Fatalf("expected positive penumbral magnitude, got %.12f", result.PenumbralMagnitude)
}
})
}
}
func TestLunarEclipseDanjonMagnitudesCloserToNASA(t *testing.T) {
testCases := []struct {
name string
jde float64
expectedType LunarEclipseType
nasaPenumbralMagnitude float64
nasaUmbralMagnitude float64
}{
{
name: "2023-10-28 partial",
jde: JDECalc(2023, 10, 28),
expectedType: LunarEclipsePartial,
nasaPenumbralMagnitude: 1.1181,
nasaUmbralMagnitude: 0.1220,
},
{
name: "2025-03-14 total",
jde: JDECalc(2025, 3, 14),
expectedType: LunarEclipseTotal,
nasaPenumbralMagnitude: 2.2595,
nasaUmbralMagnitude: 1.1784,
},
{
name: "2026-03-03 total",
jde: JDECalc(2026, 3, 3),
expectedType: LunarEclipseTotal,
nasaPenumbralMagnitude: 2.1838,
nasaUmbralMagnitude: 1.1507,
},
{
name: "2026-08-28 partial",
jde: JDECalc(2026, 8, 28),
expectedType: LunarEclipsePartial,
nasaPenumbralMagnitude: 1.9645,
nasaUmbralMagnitude: 0.9299,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
danjonResult := LunarEclipseDanjon(tc.jde)
chauvenetResult := LunarEclipseChauvenet(tc.jde)
if danjonResult.Type != tc.expectedType {
t.Fatalf("Danjon type mismatch: got %s want %s", danjonResult.Type, tc.expectedType)
}
if chauvenetResult.Type != tc.expectedType {
t.Fatalf("Chauvenet type mismatch: got %s want %s", chauvenetResult.Type, tc.expectedType)
}
danjonPenumbralError := math.Abs(danjonResult.PenumbralMagnitude - tc.nasaPenumbralMagnitude)
chauvenetPenumbralError := math.Abs(chauvenetResult.PenumbralMagnitude - tc.nasaPenumbralMagnitude)
if !(danjonPenumbralError < chauvenetPenumbralError) {
t.Fatalf("Danjon penumbral magnitude should be closer to NASA: danjon=%.6f chauvenet=%.6f nasa=%.6f", danjonResult.PenumbralMagnitude, chauvenetResult.PenumbralMagnitude, tc.nasaPenumbralMagnitude)
}
danjonUmbralError := math.Abs(danjonResult.Magnitude - tc.nasaUmbralMagnitude)
chauvenetUmbralError := math.Abs(chauvenetResult.Magnitude - tc.nasaUmbralMagnitude)
if !(danjonUmbralError < chauvenetUmbralError) {
t.Fatalf("Danjon umbral magnitude should be closer to NASA: danjon=%.6f chauvenet=%.6f nasa=%.6f", danjonResult.Magnitude, chauvenetResult.Magnitude, tc.nasaUmbralMagnitude)
}
})
}
}
func TestLunarEclipseNoEvent(t *testing.T) {
testCases := []struct {
name string
calc func(float64) LunarEclipseResult
}{
{name: "default", calc: LunarEclipse},
{name: "danjon", calc: LunarEclipseDanjon},
{name: "chauvenet", calc: LunarEclipseChauvenet},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := tc.calc(JDECalc(2023, 6, 4))
if result.Type != LunarEclipseNone {
t.Fatalf("Type mismatch: got %s want %s", result.Type, LunarEclipseNone)
}
if result.HasPenumbral || result.HasPartial || result.HasTotal {
t.Fatalf("unexpected contacts: %+v", result)
}
if result.PenumbralStart != 0 || result.PenumbralEnd != 0 || result.PartialStart != 0 || result.PartialEnd != 0 || result.TotalStart != 0 || result.TotalEnd != 0 {
t.Fatalf("expected no contact times, got %+v", result)
}
if result.Magnitude != 0 || result.PenumbralMagnitude != 0 {
t.Fatalf("expected zero magnitudes for non-eclipse, got %+v", result)
}
})
}
}
func assertCloseJD(t *testing.T, name string, got, want, tolerance float64) {
t.Helper()
if want == 0 {
if got != 0 {
t.Fatalf("%s mismatch: got %.12f want 0", name, got)
}
return
}
if math.Abs(got-want) > tolerance {
t.Fatalf("%s mismatch: got %.12f want %.12f", name, got, want)
}
}
+219
View File
@@ -0,0 +1,219 @@
package basic
import "math"
func GetLunar(year, month, day int, tz float64) (lyear, lmonth, lday int, leap bool, result string) {
julianDayEpoch := JDECalc(year, month, float64(day))
// 确定农历年份
lyear = year
adjustedYear := year
if month == 11 || month == 12 {
winterSolsticeDay := GetJQTime(year, 270) + tz
//firstNewMoonDay := TD2UT(CalcMoonS(float64(year)+11.0/12.0+5.0/30.0/12.0, 0), true) + tz
//nextNewMoonDay := TD2UT(CalcMoonS(float64(year)+1.0, 0), true) + tz
firstNewMoonDay := TD2UT(CalcMoonSHByJDE(winterSolsticeDay-16, 0), false) + tz
nextNewMoonDay := TD2UT(CalcMoonSHByJDE(firstNewMoonDay+28, 0), false) + tz
firstNewMoonDay = normalizeTimePoint(firstNewMoonDay)
nextNewMoonDay = normalizeTimePoint(nextNewMoonDay)
if winterSolsticeDay >= firstNewMoonDay && winterSolsticeDay < nextNewMoonDay && julianDayEpoch < firstNewMoonDay {
adjustedYear--
}
if winterSolsticeDay >= nextNewMoonDay && julianDayEpoch < nextNewMoonDay {
adjustedYear--
}
} else {
adjustedYear--
}
// 获取节气和朔望月数据
solarTerms := GetJieqiLoops(adjustedYear, 25)
newMoonDays := GetMoonLoops(float64(adjustedYear), 17)
// 计算冬至日期
winterSolsticeFirst := normalizeTimePoint(solarTerms[0] - 8.0/24 + tz)
winterSolsticeSecond := normalizeTimePoint(solarTerms[24] - 8.0/24 + tz)
// 规范化时间点
normalizeTimeArray(newMoonDays, tz)
normalizeTimeArray(solarTerms, tz)
// 计算朔望月范围
minMoonIndex, maxMoonIndex := 20, 0
moonCount := 0
for i := 0; i < len(newMoonDays)-1; i++ {
if (newMoonDays[i] <= winterSolsticeFirst && newMoonDays[i+1] > winterSolsticeFirst) ||
(newMoonDays[i] > winterSolsticeFirst && newMoonDays[i] < winterSolsticeSecond && newMoonDays[i+1] <= winterSolsticeSecond) {
if i <= minMoonIndex {
minMoonIndex = i
}
if i >= maxMoonIndex {
maxMoonIndex = i
}
moonCount++
}
}
// 确定闰月位置
leapMonthPos := 20
if moonCount >= 13 {
solarTermIndex, i := 0, 0
for i = minMoonIndex; i <= maxMoonIndex; i++ {
if !(newMoonDays[i] <= solarTerms[solarTermIndex] && newMoonDays[i+1] > solarTerms[solarTermIndex]) {
break
}
solarTermIndex += 2
}
leapMonthPos = i - minMoonIndex
}
// 找到当前月相索引
currentMoonIndex := 0
for currentMoonIndex = minMoonIndex; currentMoonIndex <= maxMoonIndex; currentMoonIndex++ {
if newMoonDays[currentMoonIndex] > julianDayEpoch {
break
}
}
// 计算农历月份
lmonth = currentMoonIndex - minMoonIndex - 1
shouldAdjustLeap := false
leap = false
if lmonth >= leapMonthPos {
shouldAdjustLeap = true
}
if lmonth == leapMonthPos {
leap = true
}
if lmonth < 2 {
lmonth += 11
} else {
lmonth--
}
if shouldAdjustLeap {
lmonth--
}
if lmonth <= 0 {
lmonth += 12
}
// 计算农历日期
lday = int(julianDayEpoch-newMoonDays[currentMoonIndex-1]) + 1
// 生成农历日期字符串
result = formatLunarDateString(lmonth, lday, leap)
if lmonth >= 10 && month < 3 {
lyear--
}
return
}
func GetSolar(year, month, day int, leap bool, tz float64) float64 {
adjustedYear := year
if month < 11 {
adjustedYear--
}
// 获取节气和朔望月数据
solarTerms := GetJieqiLoops(adjustedYear, 25)
newMoonDays := GetMoonLoops(float64(adjustedYear), 17)
// 计算冬至日期
winterSolsticeFirst := normalizeTimePoint(solarTerms[0] - 8.0/24 + tz)
winterSolsticeSecond := normalizeTimePoint(solarTerms[24] - 8.0/24 + tz)
// 规范化时间点
normalizeTimeArray(newMoonDays, tz)
normalizeTimeArray(solarTerms, tz)
// 计算朔望月范围
minMoonIndex, maxMoonIndex := 20, 0
moonCount := 0
for i := 0; i < 15; i++ {
if (newMoonDays[i] <= winterSolsticeFirst && newMoonDays[i+1] > winterSolsticeFirst) ||
(newMoonDays[i] > winterSolsticeFirst && newMoonDays[i] < winterSolsticeSecond && newMoonDays[i+1] <= winterSolsticeSecond) {
if i <= minMoonIndex {
minMoonIndex = i
}
if i >= maxMoonIndex {
maxMoonIndex = i
}
moonCount++
}
}
// 确定闰月位置
leapMonthPos := 20
if moonCount >= 13 {
solarTermIndex, i := 0, 0
for i = minMoonIndex; i <= maxMoonIndex; i++ {
if !(newMoonDays[i] <= solarTerms[solarTermIndex] && newMoonDays[i+1] > solarTerms[solarTermIndex]) {
break
}
solarTermIndex += 2
}
leapMonthPos = i - minMoonIndex
}
actualMonth := month
if actualMonth > 10 {
actualMonth -= 11
} else {
actualMonth++
}
// 计算实际月份索引
if leap {
actualMonth++
}
if actualMonth >= leapMonthPos && !leap {
actualMonth++
}
return newMoonDays[minMoonIndex+actualMonth] + float64(day) - 1
}
func normalizeTimeArray(timeArray []float64, tz float64) {
for idx, timeValue := range timeArray {
adjustedTime := timeValue
if tz != 8.0/24 {
adjustedTime = timeValue - 8.0/24 + tz
}
timeArray[idx] = normalizeTimePoint(adjustedTime)
}
}
func normalizeTimePoint(timePoint float64) float64 {
if timePoint-math.Floor(timePoint) > 0.5 {
return math.Floor(timePoint) + 0.5
}
return math.Floor(timePoint) - 0.5
}
func formatLunarDateString(lunarMonth, lunarDay int, isLeap bool) string {
monthNames := []string{"十", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊"}
dayPrefixes := []string{"初", "十", "廿", "三"}
var dateString string
if isLeap {
dateString += "闰"
}
if lunarMonth == 1 {
dateString += "正月"
} else {
dateString += monthNames[lunarMonth] + "月"
}
if lunarDay == 20 {
dateString += "二十"
} else if lunarDay == 10 {
dateString += "初十"
} else {
dateString += dayPrefixes[lunarDay/10] + monthNames[lunarDay%10]
}
return dateString
}
+108 -356
View File
@@ -7,162 +7,122 @@ import (
. "b612.me/astro/tools"
)
func MarsL(JD float64) float64 {
return planet.WherePlanet(3, 0, JD)
func MarsL(jd float64) float64 {
return planet.WherePlanet(3, 0, jd)
}
func MarsB(JD float64) float64 {
return planet.WherePlanet(3, 1, JD)
func MarsB(jd float64) float64 {
return planet.WherePlanet(3, 1, jd)
}
func MarsR(JD float64) float64 {
return planet.WherePlanet(3, 2, JD)
func MarsR(jd float64) float64 {
return planet.WherePlanet(3, 2, jd)
}
func AMarsX(JD float64) float64 {
l := MarsL(JD)
b := MarsB(JD)
r := MarsR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func AMarsX(jd float64) float64 {
l := MarsL(jd)
b := MarsB(jd)
r := MarsR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x
}
func AMarsY(JD float64) float64 {
func AMarsY(jd float64) float64 {
l := MarsL(JD)
b := MarsB(JD)
r := MarsR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
l := MarsL(jd)
b := MarsB(jd)
r := MarsR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y
}
func AMarsZ(JD float64) float64 {
//l := MarsL(JD)
b := MarsB(JD)
r := MarsR(JD)
// el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func AMarsZ(jd float64) float64 {
//l := MarsL(jd)
b := MarsB(jd)
r := MarsR(jd)
// el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
z := r*Sin(b) - er*Sin(eb)
return z
}
func AMarsXYZ(JD float64) (float64, float64, float64) {
l := MarsL(JD)
b := MarsB(JD)
r := MarsR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func AMarsXYZ(jd float64) (float64, float64, float64) {
l := MarsL(jd)
b := MarsB(jd)
r := MarsR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb)
return x, y, z
}
func MarsApparentRa(JD float64) float64 {
lo, bo := MarsApparentLoBo(JD)
sita := Sita(JD)
ra := math.Atan2((Sin(lo)*Cos(sita) - Tan(bo)*Sin(sita)), Cos(lo))
func MarsApparentRa(jd float64) float64 {
lo, bo := MarsApparentLoBo(jd)
eps := TrueObliquity(jd)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
return Limit360(ra)
}
func MarsApparentDec(JD float64) float64 {
lo, bo := MarsApparentLoBo(JD)
sita := Sita(JD)
dec := ArcSin(Sin(bo)*Cos(sita) + Cos(bo)*Sin(sita)*Sin(lo))
func MarsApparentDec(jd float64) float64 {
lo, bo := MarsApparentLoBo(jd)
eps := TrueObliquity(jd)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec
}
func MarsApparentRaDec(JD float64) (float64, float64) {
lo, bo := MarsApparentLoBo(JD)
sita := Sita(JD)
ra := math.Atan2((Sin(lo)*Cos(sita) - Tan(bo)*Sin(sita)), Cos(lo))
func MarsApparentRaDec(jd float64) (float64, float64) {
lo, bo := MarsApparentLoBo(jd)
eps := TrueObliquity(jd)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
dec := ArcSin(Sin(bo)*Cos(sita) + Cos(bo)*Sin(sita)*Sin(lo))
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return Limit360(ra), dec
}
func EarthMarsAway(JD float64) float64 {
x, y, z := AMarsXYZ(JD)
to := math.Sqrt(x*x + y*y + z*z)
return to
func EarthMarsAway(jd float64) float64 {
return planetEarthAwayExplicitN(3, jd, -1)
}
func MarsApparentLo(JD float64) float64 {
x, y, z := AMarsXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMarsXYZ(JD - to)
lo := math.Atan2(y, x)
//bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180.0 / math.Pi
//bo = bo * 180 / math.Pi
lo = Limit360(lo) + Nutation2000Bi(JD)
//lo-=GXCLo(lo,bo,JD)/3600;
//bo+=GXCBo(lo,bo,JD);
return lo
func MarsApparentLo(jd float64) float64 {
geo, _ := planetApparentGeocentricPositionN(3, jd, -1)
return geo.lo
}
func MarsApparentBo(JD float64) float64 {
x, y, z := AMarsXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMarsXYZ(JD - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,JD);
//bo+=GXCBo(lo,bo,JD)/3600;
//lo+=Nutation2000Bi(JD);
return bo
func MarsApparentBo(jd float64) float64 {
geo, _ := planetApparentGeocentricPositionN(3, jd, -1)
return geo.bo
}
func MarsApparentLoBo(JD float64) (float64, float64) {
x, y, z := AMarsXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMarsXYZ(JD - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo -= GXCLo(lo, bo, JD) / 3600
//bo += GXCBo(lo, bo, JD)
lo += Nutation2000Bi(JD)
return lo, bo
func MarsApparentLoBo(jd float64) (float64, float64) {
geo, _ := planetApparentGeocentricPositionN(3, jd, -1)
return geo.lo, geo.bo
}
func MarsTrueLoBo(JD float64) (float64, float64) {
x, y, z := AMarsXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMarsXYZ(JD - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
return lo, bo
func MarsTrueLoBo(jd float64) (float64, float64) {
geo, _ := planetTrueGeocentricPositionN(3, jd, -1)
return geo.lo, geo.bo
}
func MarsTrueLo(JD float64) float64 {
x, y, _ := AMarsXYZ(JD)
lo := math.Atan2(y, x)
lo = lo * 180 / math.Pi
lo = Limit360(lo)
return lo
func MarsTrueLo(jd float64) float64 {
geo, _ := planetTrueGeocentricPositionN(3, jd, -1)
return geo.lo
}
func MarsMag(JD float64) float64 {
AwaySun := MarsR(JD)
AwayEarth := EarthMarsAway(JD)
Away := planet.WherePlanet(-1, 2, JD)
i := (AwaySun*AwaySun + AwayEarth*AwayEarth - Away*Away) / (2 * AwaySun * AwayEarth)
func MarsMag(jd float64) float64 {
sunDistance := MarsR(jd)
earthDistance := EarthMarsAway(jd)
earthSunDistance := planet.WherePlanet(-1, 2, jd)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(i)
Mag := -1.52 + 5*math.Log10(AwaySun*AwayEarth) + 0.016*i
return FloatRound(Mag, 2)
mag := -1.52 + 5*math.Log10(sunDistance*earthDistance) + 0.016*i
return FloatRound(mag, 2)
}
func MarsHeight(jde, lon, lat, timezone float64) float64 {
@@ -172,10 +132,10 @@ func MarsHeight(jde, lon, lat, timezone float64) float64 {
ra, dec := MarsApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
// 计算时角
H := Limit360(st - ra)
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(H)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(H)
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(hourAngle)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(hourAngle)
return ArcSin(sinHeight)
}
@@ -186,271 +146,63 @@ func MarsAzimuth(jde, lon, lat, timezone float64) float64 {
ra, dec := MarsApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
// 计算时角
H := Limit360(st - ra)
hourAngle := Limit360(st - ra)
// 三角转换公式
tanAzimuth := Sin(H) / (Cos(H)*Sin(lat) - Tan(dec)*Cos(lat))
Azimuth := ArcTan(tanAzimuth)
if Azimuth < 0 {
if H/15 < 12 {
return Azimuth + 360
tanAzimuth := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(dec)*Cos(lat))
azimuth := ArcTan(tanAzimuth)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
}
return Azimuth + 180
return azimuth + 180
}
if H/15 < 12 {
return Azimuth + 180
if hourAngle/15 < 12 {
return azimuth + 180
}
return Azimuth
return azimuth
}
func MarsHourAngle(JD, Lon, TZ float64) float64 {
startime := Limit360(ApparentSiderealTime(JD-TZ/24)*15 + Lon)
timeangle := startime - MarsApparentRa(TD2UT(JD-TZ/24.0, true))
if timeangle < 0 {
timeangle += 360
func MarsHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon)
hourAngle := siderealLongitude - MarsApparentRa(TD2UT(jd-timezone/24.0, true))
if hourAngle < 0 {
hourAngle += 360
}
return timeangle
return hourAngle
}
func MarsCulminationTime(jde, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5
JD1 := jde + Limit360(360-MarsHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
limitHA := func(jde, lon, timezone float64) float64 {
ha := MarsHourAngle(jde, lon, timezone)
if ha < 180 {
ha += 360
estimateJD := jde + Limit360(360-MarsHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 {
currentHourAngle := MarsHourAngle(jde, lon, timezone)
if currentHourAngle < 180 {
currentHourAngle += 360
}
return ha
return currentHourAngle
}
for {
JD0 := JD1
stDegree := limitHA(JD0, lon, timezone) - 360
stDegreep := (limitHA(JD0+0.000005, lon, timezone) - limitHA(JD0-0.000005, lon, timezone)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
prevJD := estimateJD
hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360
hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001
estimateJD = prevJD - hourAngleDelta/hourAngleSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return JD1
return estimateJD
}
func MarsRiseTime(JD, Lon, Lat, TZ, ZS, HEI float64) float64 {
return marsRiseDown(JD, Lon, Lat, TZ, ZS, HEI, true)
func MarsRiseTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return marsRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true)
}
func MarsDownTime(JD, Lon, Lat, TZ, ZS, HEI float64) float64 {
return marsRiseDown(JD, Lon, Lat, TZ, ZS, HEI, false)
func MarsSetTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return marsRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false)
}
func marsRiseDown(JD, Lon, Lat, TZ, ZS, HEI float64, isRise bool) float64 {
var An float64
JD = math.Floor(JD) + 0.5
ntz := math.Round(Lon / 15)
if ZS != 0 {
An = -0.8333
}
An = An - HeightDegreeByLat(HEI, Lat)
tztime := MarsCulminationTime(JD, Lon, ntz)
if MarsHeight(tztime, Lon, Lat, ntz) < An {
return -2 //极夜
}
if MarsHeight(tztime-0.5, Lon, Lat, ntz) > An {
return -1 //极昼
}
dec := HSunApparentDec(TD2UT(tztime-ntz/24, true))
//(sin(ho)-sin(φ)*sin(δ2))/(cos(φ)*cos(δ2))
tmp := (Sin(An) - Sin(dec)*Sin(Lat)) / (Cos(dec) * Cos(Lat))
var rise float64
if math.Abs(tmp) <= 1 {
rzsc := ArcCos(tmp) / 15
if isRise {
rise = tztime - rzsc/24 - 25.0/24.0/60.0
} else {
rise = tztime + rzsc/24 - 25.0/24.0/60.0
}
} else {
rise = tztime
i := 0
//TODO:使用二分法计算
for MarsHeight(rise, Lon, Lat, ntz) > An {
i++
if isRise {
rise -= 15.0 / 60.0 / 24.0
} else {
rise += 15.0 / 60.0 / 24.0
}
if i > 48 {
break
}
}
}
JD1 := rise
for {
JD0 := JD1
stDegree := MarsHeight(JD0, Lon, Lat, ntz) - An
stDegreep := (MarsHeight(JD0+0.000005, Lon, Lat, ntz) - MarsHeight(JD0-0.000005, Lon, Lat, ntz)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
break
}
}
return JD1 - ntz/24 + TZ/24
}
// Pos
const MARS_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 686.98))
func marsConjunction(jde, degree float64, next uint8) float64 {
//0=last 1=next
decSub := func(jde float64, degree float64, filter bool) float64 {
sub := Limit360(Limit360(MarsApparentLo(jde)-HSunApparentLo(jde)) - degree)
if filter {
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
}
return sub
}
dayCost := MARS_S_PERIOD / 360
nowSub := decSub(jde, degree, false)
if next == 0 {
jde -= (360 - nowSub) * dayCost
} else {
jde += dayCost * nowSub
}
JD1 := jde
for {
JD0 := JD1
stDegree := decSub(JD0, degree, true)
stDegreep := (decSub(JD0+0.000005, degree, true) - decSub(JD0-0.000005, degree, true)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
break
}
}
return TD2UT(JD1, false)
}
func LastMarsConjunction(jde float64) float64 {
return marsConjunction(jde, 0, 0)
}
func NextMarsConjunction(jde float64) float64 {
return marsConjunction(jde, 0, 1)
}
func LastMarsOpposition(jde float64) float64 {
return marsConjunction(jde, 180, 0)
}
func NextMarsOpposition(jde float64) float64 {
return marsConjunction(jde, 180, 1)
}
func NextMarsEasternQuadrature(jde float64) float64 {
return marsConjunction(jde, 90, 1)
}
func LastMarsEasternQuadrature(jde float64) float64 {
return marsConjunction(jde, 90, 0)
}
func NextMarsWesternQuadrature(jde float64) float64 {
return marsConjunction(jde, 270, 1)
}
func LastMarsWesternQuadrature(jde float64) float64 {
return marsConjunction(jde, 270, 0)
}
func marsRetrograde(jde float64, isLeft bool) float64 {
//0=last 1=next
decSub := func(jde float64, val float64) float64 {
sub := MarsApparentRa(jde+val) - MarsApparentRa(jde-val)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * val)
}
jde = NextMarsOpposition(jde)
if isLeft {
jde -= 60
} else {
jde += 60
}
for {
nowSub := decSub(jde, 1.0/86400.0)
if math.Abs(nowSub) > 0.55 {
jde += 2
continue
}
break
}
JD1 := jde
for {
JD0 := JD1
stDegree := decSub(JD0, 2.0/86400.0)
stDegreep := (decSub(JD0+15.0/86400.0, 2.0/86400.0) - decSub(JD0-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 30.0/86400.0 {
break
}
}
JD1 = JD1 - 15.0/86400.0
min := JD1
minRa := 100.0
for i := 0.0; i < 60.0; i++ {
tmp := decSub(JD1+i*0.5/86400.0, 0.5/86400.0)
if math.Abs(tmp) < math.Abs(minRa) {
minRa = tmp
min = JD1 + i*0.5/86400.0
}
}
return TD2UT(min, false)
}
func NextMarsRetrogradeToPrograde(jde float64) float64 {
date := marsRetrograde(jde, false)
if date < jde {
op := NextMarsOpposition(jde)
return marsRetrograde(op+10, false)
}
return date
}
func LastMarsRetrogradeToPrograde(jde float64) float64 {
jde = LastMarsOpposition(jde) - 10
date := marsRetrograde(jde, false)
if date > jde {
op := LastMarsOpposition(jde)
return marsRetrograde(op-10, false)
}
return date
}
func NextMarsProgradeToRetrograde(jde float64) float64 {
date := marsRetrograde(jde, true)
if date < jde {
op := NextMarsOpposition(jde)
return marsRetrograde(op+10, true)
}
return date
}
func LastMarsProgradeToRetrograde(jde float64) float64 {
jde = LastMarsOpposition(jde) - 10
date := marsRetrograde(jde, true)
if date > jde {
op := LastMarsOpposition(jde)
return marsRetrograde(op-10, true)
}
return date
func marsRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool) (float64, error) {
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, MarsCulminationTime, MarsHeight, MarsApparentDec)
}
+35
View File
@@ -0,0 +1,35 @@
package basic
import "testing"
func BenchmarkMarsPhaseFamily(b *testing.B) {
cases := marsEventCases()[:8]
samples := marsEventSamples()
var sink float64
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, sample := range samples {
jd := marsEventSampleTTJD(sample)
for _, event := range cases {
sink += event.fn(jd)
}
}
}
_ = sink
}
func BenchmarkMarsRetrogradeFamily(b *testing.B) {
cases := marsEventCases()[8:]
samples := marsEventSamples()
var sink float64
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, sample := range samples {
jd := marsEventSampleTTJD(sample)
for _, event := range cases {
sink += event.fn(jd)
}
}
}
_ = sink
}
+53
View File
@@ -0,0 +1,53 @@
package basic
import "time"
type marsEventFunc func(float64) float64
type marsEventCase struct {
name string
tolerance float64
fn marsEventFunc
}
func marsEventSamples() []time.Time {
start := time.Date(1992, 8, 17, 9, 21, 45, 678000000, time.UTC)
samples := make([]time.Time, 0, 96)
for i := 0; i < 48; i++ {
d := start.AddDate(0, 0, i*311)
d = d.Add(time.Duration((i%8)*4)*time.Hour + time.Duration((i%10)*9)*time.Minute + time.Duration((i%12)*17)*time.Second)
samples = append(samples, d)
}
extraStart := start.AddDate(0, 0, 53)
for i := 0; i < 48; i++ {
d := extraStart.AddDate(0, 0, i*197)
d = d.Add(time.Duration((i%7)*5)*time.Hour + time.Duration((i%9)*14)*time.Minute + time.Duration((i%16)*23)*time.Second)
samples = append(samples, d)
}
return samples
}
func marsEventSampleTTJD(date time.Time) float64 {
return TD2UT(Date2JDE(date.UTC()), true)
}
func marsEventCases() []marsEventCase {
const (
conjunctionTolerance = 0.00001
searchTolerance = 30.0 / 86400.0
)
return []marsEventCase{
{name: "LastMarsConjunction", tolerance: conjunctionTolerance, fn: LastMarsConjunction},
{name: "NextMarsConjunction", tolerance: conjunctionTolerance, fn: NextMarsConjunction},
{name: "LastMarsOpposition", tolerance: conjunctionTolerance, fn: LastMarsOpposition},
{name: "NextMarsOpposition", tolerance: conjunctionTolerance, fn: NextMarsOpposition},
{name: "LastMarsEasternQuadrature", tolerance: conjunctionTolerance, fn: LastMarsEasternQuadrature},
{name: "NextMarsEasternQuadrature", tolerance: conjunctionTolerance, fn: NextMarsEasternQuadrature},
{name: "LastMarsWesternQuadrature", tolerance: conjunctionTolerance, fn: LastMarsWesternQuadrature},
{name: "NextMarsWesternQuadrature", tolerance: conjunctionTolerance, fn: NextMarsWesternQuadrature},
{name: "LastMarsProgradeToRetrograde", tolerance: searchTolerance, fn: LastMarsProgradeToRetrograde},
{name: "NextMarsProgradeToRetrograde", tolerance: searchTolerance, fn: NextMarsProgradeToRetrograde},
{name: "LastMarsRetrogradeToPrograde", tolerance: searchTolerance, fn: LastMarsRetrogradeToPrograde},
{name: "NextMarsRetrogradeToPrograde", tolerance: searchTolerance, fn: NextMarsRetrogradeToPrograde},
}
}
+57
View File
@@ -0,0 +1,57 @@
package basic
import (
"encoding/json"
"math"
"os"
"testing"
)
type marsEventBaselineSample struct {
InputUTC string `json:"input_utc"`
TTJDBits uint64 `json:"tt_jd_bits"`
Events map[string]uint64 `json:"events"`
}
type marsEventBaseline struct {
Samples []marsEventBaselineSample `json:"samples"`
}
func loadMarsEventBaseline(t *testing.T) marsEventBaseline {
t.Helper()
data, err := os.ReadFile("testdata/mars_event_baseline.json")
if err != nil {
t.Fatal(err)
}
var baseline marsEventBaseline
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatal(err)
}
if len(baseline.Samples) == 0 {
t.Fatal("empty mars event baseline")
}
return baseline
}
func TestMarsEventBaselineRegression(t *testing.T) {
baseline := loadMarsEventBaseline(t)
cases := marsEventCases()
for _, sample := range baseline.Samples {
jd := math.Float64frombits(sample.TTJDBits)
for _, event := range cases {
wantBits, ok := sample.Events[event.name]
if !ok {
t.Fatalf("%s missing baseline event %s", sample.InputUTC, event.name)
}
want := math.Float64frombits(wantBits)
got := event.fn(jd)
diff := math.Abs(got - want)
if diff > event.tolerance {
t.Fatalf("%s %s diff %.12f > tolerance %.12f", sample.InputUTC, event.name, diff, event.tolerance)
}
}
}
}
+243
View File
@@ -0,0 +1,243 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
// Pos
const (
MARS_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 686.98))
marsEventSearchN = 16
marsPhaseCoarseTolerance = 30.0 / 86400.0
)
func marsSunLongitudeDelta(jde, degree float64, filter bool) float64 {
sub := Limit360(Limit360(MarsApparentLo(jde)-HSunApparentLo(jde)) - degree)
if filter {
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
}
return sub
}
func marsSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64 {
sub := Limit360(Limit360(MarsApparentLoN(jde, n)-HSunApparentLoN(jde, n)) - degree)
if filter {
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
}
return sub
}
func marsRADerivative(jde, val float64) float64 {
sub := MarsApparentRa(jde+val) - MarsApparentRa(jde-val)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * val)
}
func marsRADerivativeN(jde, val float64, n int) float64 {
sub := MarsApparentRaN(jde+val, n) - MarsApparentRaN(jde-val, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * val)
}
func marsConjunctionFull(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := MARS_S_PERIOD / 360
currentDelta := marsSunLongitudeDelta(jde, degree, false)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
for {
prevJD := estimateJD
longitudeDelta := marsSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJD+0.000005, degree, true) - marsSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
estimateJD = prevJD - longitudeDelta/longitudeSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return TD2UT(estimateJD, false)
}
func marsConjunction(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := MARS_S_PERIOD / 360
currentDelta := marsSunLongitudeDelta(jde, degree, false)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
for {
prevJD := estimateJD
longitudeDelta := marsSunLongitudeDeltaN(prevJD, degree, true, marsEventSearchN)
longitudeSlope := (marsSunLongitudeDeltaN(prevJD+0.000005, degree, true, marsEventSearchN) - marsSunLongitudeDeltaN(prevJD-0.000005, degree, true, marsEventSearchN)) / 0.00001
estimateJD = prevJD - longitudeDelta/longitudeSlope
if math.Abs(estimateJD-prevJD) <= marsPhaseCoarseTolerance {
break
}
}
for {
prevJD := estimateJD
longitudeDelta := marsSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJD+0.000005, degree, true) - marsSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
estimateJD = prevJD - longitudeDelta/longitudeSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return TD2UT(estimateJD, false)
}
func LastMarsConjunction(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 0, marsConjunction)
}
func NextMarsConjunction(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 0, marsConjunction)
}
func LastMarsOpposition(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 180, marsConjunction)
}
func NextMarsOpposition(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 180, marsConjunction)
}
func NextMarsEasternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 90, marsConjunction)
}
func LastMarsEasternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 90, marsConjunction)
}
func NextMarsWesternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 270, marsConjunction)
}
func LastMarsWesternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 270, marsConjunction)
}
func marsRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
jde := oppositionJD
if searchBeforeOpposition {
jde -= 60
} else {
jde += 60
}
for {
currentRate := marsRADerivative(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
rateValue := marsRADerivative(prevJD, 2.0/86400.0)
rateSlope := (marsRADerivative(prevJD+15.0/86400.0, 2.0/86400.0) - marsRADerivative(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return marsRADerivative(jd, 0.5/86400.0)
})
return TD2UT(bestJD, false)
}
func marsOppositionFromBefore(oppositionJD float64) float64 {
return marsConjunctionFull(eventUTLastQueryTT(oppositionJD), 180, 1)
}
func marsOppositionFromAfter(oppositionJD float64) float64 {
return marsConjunctionFull(eventUTNextQueryTT(oppositionJD), 180, 0)
}
func NextMarsRetrogradeToPrograde(jde float64) float64 {
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) {
sameOppositionJD := marsOppositionFromBefore(lastOppositionJD)
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, false))
}
if !eventUTQueryAfterOrEqual(date, jde) {
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
return marsRetrogradeAroundOpposition(nextOppositionJD, false)
}
return date
}
func LastMarsRetrogradeToPrograde(jde float64) float64 {
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) {
sameOppositionJD := marsOppositionFromBefore(lastOppositionJD)
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, false))
}
if !eventUTQueryBeforeOrEqual(date, jde) {
previousOppositionJD := marsConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return marsRetrogradeAroundOpposition(previousOppositionJD, false)
}
return date
}
func NextMarsProgradeToRetrograde(jde float64) float64 {
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) {
sameOppositionJD := marsOppositionFromAfter(nextOppositionJD)
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, true))
}
if !eventUTQueryAfterOrEqual(date, jde) {
followingOppositionJD := marsConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return marsRetrogradeAroundOpposition(followingOppositionJD, true)
}
return date
}
func LastMarsProgradeToRetrograde(jde float64) float64 {
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) {
sameOppositionJD := marsOppositionFromAfter(nextOppositionJD)
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, true))
}
if !eventUTQueryBeforeOrEqual(date, jde) {
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
return marsRetrogradeAroundOpposition(lastOppositionJD, true)
}
return date
}
-21
View File
@@ -1,21 +0,0 @@
package basic
import (
"fmt"
"testing"
)
func TestMars(t *testing.T) {
jde := GetNowJDE() - 6000
/*
fmt.Println(JDE2Date(VenusCulminationTime(jde, 115, 8)))
fmt.Println(JDE2Date(VenusRiseTime(jde, 115, 23, 8, 0, 0)))
fmt.Println(JDE2Date(VenusDownTime(jde, 115, 23, 8, 0, 0)))
fmt.Println("----------------")
*/
//LastVenusConjunction(2.4596600340162036e+06)
//fmt.Println(2.4590359532407406e+06, JDE2Date(2.4590359532407406e+06), JDE2Date(NextVenusRetrograde(2.4590359532407406e+06)))
for i := 0.00; i < 1; i++ {
fmt.Println(jde+i*740, JDE2Date(jde+i*740), JDE2Date(LastMarsProgradeToRetrograde(jde+i*740)))
}
}
+99 -516
View File
@@ -7,135 +7,105 @@ import (
. "b612.me/astro/tools"
)
func MercuryL(JD float64) float64 {
return planet.WherePlanet(1, 0, JD)
func MercuryL(jd float64) float64 {
return planet.WherePlanet(1, 0, jd)
}
func MercuryB(JD float64) float64 {
return planet.WherePlanet(1, 1, JD)
func MercuryB(jd float64) float64 {
return planet.WherePlanet(1, 1, jd)
}
func MercuryR(JD float64) float64 {
return planet.WherePlanet(1, 2, JD)
func MercuryR(jd float64) float64 {
return planet.WherePlanet(1, 2, jd)
}
func AMercuryX(JD float64) float64 {
l := MercuryL(JD)
b := MercuryB(JD)
r := MercuryR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func AMercuryX(jd float64) float64 {
l := MercuryL(jd)
b := MercuryB(jd)
r := MercuryR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x
}
func AMercuryY(JD float64) float64 {
func AMercuryY(jd float64) float64 {
l := MercuryL(JD)
b := MercuryB(JD)
r := MercuryR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
l := MercuryL(jd)
b := MercuryB(jd)
r := MercuryR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y
}
func AMercuryZ(JD float64) float64 {
//l := MercuryL(JD)
b := MercuryB(JD)
r := MercuryR(JD)
// el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func AMercuryZ(jd float64) float64 {
//l := MercuryL(jd)
b := MercuryB(jd)
r := MercuryR(jd)
// el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
z := r*Sin(b) - er*Sin(eb)
return z
}
func AMercuryXYZ(JD float64) (float64, float64, float64) {
l := MercuryL(JD)
b := MercuryB(JD)
r := MercuryR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func AMercuryXYZ(jd float64) (float64, float64, float64) {
l := MercuryL(jd)
b := MercuryB(jd)
r := MercuryR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb)
return x, y, z
}
func MercuryApparentRa(JD float64) float64 {
lo, bo := MercuryApparentLoBo(JD)
return LoToRa(JD, lo, bo)
func MercuryApparentRa(jd float64) float64 {
lo, bo := MercuryApparentLoBo(jd)
return LoToRa(jd, lo, bo)
}
func MercuryApparentDec(JD float64) float64 {
lo, bo := MercuryApparentLoBo(JD)
sita := Sita(JD)
dec := ArcSin(Sin(bo)*Cos(sita) + Cos(bo)*Sin(sita)*Sin(lo))
func MercuryApparentDec(jd float64) float64 {
lo, bo := MercuryApparentLoBo(jd)
eps := TrueObliquity(jd)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec
}
func MercuryApparentRaDec(JD float64) (float64, float64) {
lo, bo := MercuryApparentLoBo(JD)
return LoBoToRaDec(JD, lo, bo)
func MercuryApparentRaDec(jd float64) (float64, float64) {
lo, bo := MercuryApparentLoBo(jd)
return LoBoToRaDec(jd, lo, bo)
}
func EarthMercuryAway(JD float64) float64 {
x, y, z := AMercuryXYZ(JD)
to := math.Sqrt(x*x + y*y + z*z)
return to
func EarthMercuryAway(jd float64) float64 {
return planetEarthAwayExplicitN(1, jd, -1)
}
func MercuryApparentLo(JD float64) float64 {
x, y, z := AMercuryXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMercuryXYZ(JD - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,JD)/3600;
//bo+=GXCBo(lo,bo,JD);
lo += Nutation2000Bi(JD)
return lo
func MercuryApparentLo(jd float64) float64 {
geo, _ := planetApparentGeocentricPositionN(1, jd, -1)
return geo.lo
}
func MercuryApparentBo(JD float64) float64 {
x, y, z := AMercuryXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMercuryXYZ(JD - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,JD);
//bo+=GXCBo(lo,bo,JD)/3600;
//lo+=Nutation2000Bi(JD);
return bo
func MercuryApparentBo(jd float64) float64 {
geo, _ := planetApparentGeocentricPositionN(1, jd, -1)
return geo.bo
}
func MercuryApparentLoBo(JD float64) (float64, float64) {
x, y, z := AMercuryXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMercuryXYZ(JD - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo) + Nutation2000Bi(JD)
//lo-=GXCLo(lo,bo,JD)/3600;
//bo+=GXCBo(lo,bo,JD);
return lo, bo
func MercuryApparentLoBo(jd float64) (float64, float64) {
geo, _ := planetApparentGeocentricPositionN(1, jd, -1)
return geo.lo, geo.bo
}
func MercuryMag(JD float64) float64 {
AwaySun := MercuryR(JD)
AwayEarth := EarthMercuryAway(JD)
Away := planet.WherePlanet(-1, 2, JD)
i := (AwaySun*AwaySun + AwayEarth*AwayEarth - Away*Away) / (2 * AwaySun * AwayEarth)
func MercuryMag(jd float64) float64 {
sunDistance := MercuryR(jd)
earthDistance := EarthMercuryAway(jd)
earthSunDistance := planet.WherePlanet(-1, 2, jd)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(i)
Mag := -0.42 + 5*math.Log10(AwaySun*AwayEarth) + 0.0380*i - 0.000273*i*i + 0.000002*i*i*i
return FloatRound(Mag, 2)
mag := -0.42 + 5*math.Log10(sunDistance*earthDistance) + 0.0380*i - 0.000273*i*i + 0.000002*i*i*i
return FloatRound(mag, 2)
}
func MercuryHeight(jde, lon, lat, timezone float64) float64 {
@@ -145,10 +115,10 @@ func MercuryHeight(jde, lon, lat, timezone float64) float64 {
ra, dec := MercuryApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
// 计算时角
H := Limit360(st - ra)
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(H)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(H)
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(hourAngle)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(hourAngle)
return ArcSin(sinHeight)
}
@@ -159,450 +129,63 @@ func MercuryAzimuth(jde, lon, lat, timezone float64) float64 {
ra, dec := MercuryApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
// 计算时角
H := Limit360(st - ra)
hourAngle := Limit360(st - ra)
// 三角转换公式
tanAzimuth := Sin(H) / (Cos(H)*Sin(lat) - Tan(dec)*Cos(lat))
Azimuth := ArcTan(tanAzimuth)
if Azimuth < 0 {
if H/15 < 12 {
return Azimuth + 360
tanAzimuth := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(dec)*Cos(lat))
azimuth := ArcTan(tanAzimuth)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
}
return Azimuth + 180
return azimuth + 180
}
if H/15 < 12 {
return Azimuth + 180
if hourAngle/15 < 12 {
return azimuth + 180
}
return Azimuth
return azimuth
}
func MercuryHourAngle(JD, Lon, TZ float64) float64 {
startime := Limit360(ApparentSiderealTime(JD-TZ/24)*15 + Lon)
timeangle := startime - MercuryApparentRa(TD2UT(JD-TZ/24.0, true))
if timeangle < 0 {
timeangle += 360
func MercuryHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon)
hourAngle := siderealLongitude - MercuryApparentRa(TD2UT(jd-timezone/24.0, true))
if hourAngle < 0 {
hourAngle += 360
}
return timeangle
return hourAngle
}
func MercuryCulminationTime(jde, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5
JD1 := jde + Limit360(360-MercuryHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
limitHA := func(jde, lon, timezone float64) float64 {
ha := MercuryHourAngle(jde, lon, timezone)
if ha < 180 {
ha += 360
estimateJD := jde + Limit360(360-MercuryHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 {
currentHourAngle := MercuryHourAngle(jde, lon, timezone)
if currentHourAngle < 180 {
currentHourAngle += 360
}
return ha
return currentHourAngle
}
for {
JD0 := JD1
stDegree := limitHA(JD0, lon, timezone) - 360
stDegreep := (limitHA(JD0+0.000005, lon, timezone) - limitHA(JD0-0.000005, lon, timezone)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
prevJD := estimateJD
hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360
hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001
estimateJD = prevJD - hourAngleDelta/hourAngleSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return JD1
return estimateJD
}
func MercuryRiseTime(JD, Lon, Lat, TZ, ZS, HEI float64) float64 {
return mercuryRiseDown(JD, Lon, Lat, TZ, ZS, HEI, true)
func MercuryRiseTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return mercuryRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true)
}
func MercuryDownTime(JD, Lon, Lat, TZ, ZS, HEI float64) float64 {
return mercuryRiseDown(JD, Lon, Lat, TZ, ZS, HEI, false)
func MercurySetTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return mercuryRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false)
}
func mercuryRiseDown(JD, Lon, Lat, TZ, ZS, HEI float64, isRise bool) float64 {
var An float64
JD = math.Floor(JD) + 0.5
ntz := math.Round(Lon / 15)
if ZS != 0 {
An = -0.8333
}
An = An - HeightDegreeByLat(HEI, Lat)
tztime := MercuryCulminationTime(JD, Lon, ntz)
if MercuryHeight(tztime, Lon, Lat, ntz) < An {
return -2 //极夜
}
if MercuryHeight(tztime-0.5, Lon, Lat, ntz) > An {
return -1 //极昼
}
dec := HSunApparentDec(TD2UT(tztime-ntz/24, true))
//(sin(ho)-sin(φ)*sin(δ2))/(cos(φ)*cos(δ2))
tmp := (Sin(An) - Sin(dec)*Sin(Lat)) / (Cos(dec) * Cos(Lat))
var rise float64
if math.Abs(tmp) <= 1 {
rzsc := ArcCos(tmp) / 15
if isRise {
rise = tztime - rzsc/24 - 25.0/24.0/60.0
} else {
rise = tztime + rzsc/24 - 25.0/24.0/60.0
}
} else {
rise = tztime
i := 0
//TODO:使用二分法计算
for MercuryHeight(rise, Lon, Lat, ntz) > An {
i++
if isRise {
rise -= 15.0 / 60.0 / 24.0
} else {
rise += 15.0 / 60.0 / 24.0
}
if i > 48 {
break
}
}
}
JD1 := rise
for {
JD0 := JD1
stDegree := MercuryHeight(JD0, Lon, Lat, ntz) - An
stDegreep := (MercuryHeight(JD0+0.000005, Lon, Lat, ntz) - MercuryHeight(JD0-0.000005, Lon, Lat, ntz)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
break
}
}
return JD1 - ntz/24 + TZ/24
}
// Pos
const MERCURY_S_PERIOD = 1 / ((1 / 87.9691) - (1 / 365.256363004))
func mercuryConjunction(jde float64, next uint8) float64 {
//0=last 1=next
decSub := func(jde float64) float64 {
sub := Limit360(MercuryApparentLo(jde) - HSunApparentLo(jde))
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub
}
nowSub := decSub(jde)
// pos 大于0:远离太阳 小于0:靠近太阳
pos := math.Abs(decSub(jde+1/86400.0)) - math.Abs(nowSub)
if pos >= 0 && next == 1 && nowSub > 0 {
jde += MERCURY_S_PERIOD/8.0 + 2
}
if pos >= 0 && next == 1 && nowSub < 0 {
jde += MERCURY_S_PERIOD/6.0 + 2
}
if pos <= 0 && next == 0 && nowSub < 0 {
jde -= MERCURY_S_PERIOD/8.0 + 2
}
if pos <= 0 && next == 0 && nowSub > 0 {
jde -= MERCURY_S_PERIOD/6.0 + 2
}
for {
nowSub := decSub(jde)
pos := math.Abs(decSub(jde+1/86400.0)) - math.Abs(nowSub)
if math.Abs(nowSub) > 12 || (pos > 0 && next == 1) || (pos < 0 && next == 0) {
if next == 1 {
jde += 2
} else {
jde -= 2
}
continue
}
break
}
JD1 := jde
for {
JD0 := JD1
stDegree := decSub(JD0)
stDegreep := (decSub(JD0+0.000005) - decSub(JD0-0.000005)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
break
}
}
return TD2UT(JD1, false)
}
func LastMercuryConjunction(jde float64) float64 {
return mercuryConjunction(jde, 0)
}
func NextMercuryConjunction(jde float64) float64 {
return mercuryConjunction(jde, 1)
}
func NextMercuryInferiorConjunction(jde float64) float64 {
date := NextMercuryConjunction(jde)
if EarthMercuryAway(date) > EarthAway(date) {
return NextMercuryConjunction(date + 2)
}
return date
}
func NextMercurySuperiorConjunction(jde float64) float64 {
date := NextMercuryConjunction(jde)
if EarthMercuryAway(date) < EarthAway(date) {
return NextMercuryConjunction(date + 2)
}
return date
}
func LastMercuryInferiorConjunction(jde float64) float64 {
date := LastMercuryConjunction(jde)
if EarthMercuryAway(date) > EarthAway(date) {
return LastMercuryConjunction(date - 2)
}
return date
}
func LastMercurySuperiorConjunction(jde float64) float64 {
date := LastMercuryConjunction(jde)
if EarthMercuryAway(date) < EarthAway(date) {
return LastMercuryConjunction(date - 2)
}
return date
}
func mercuryRetrograde(jde float64) float64 {
//0=last 1=next
decSunSub := func(jde float64) float64 {
sub := Limit360(MercuryApparentRa(jde) - SunApparentRa(jde))
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub
}
decSub := func(jde float64, val float64) float64 {
sub := MercuryApparentRa(jde+val) - MercuryApparentRa(jde-val)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * val)
}
lastHe := LastMercuryConjunction(jde)
nextHe := NextMercuryConjunction(jde)
nowSub := decSunSub(jde)
if nowSub > 0 {
jde = lastHe + ((nextHe - lastHe) / 5.0 * 3.5)
} else {
jde = lastHe + ((nextHe - lastHe) / 5.5)
}
for {
nowSub := decSub(jde, 1.0/86400.0)
if math.Abs(nowSub) > 0.55 {
jde += 2
continue
}
break
}
JD1 := jde
for {
JD0 := JD1
stDegree := decSub(JD0, 2.0/86400.0)
stDegreep := (decSub(JD0+15.0/86400.0, 2.0/86400.0) - decSub(JD0-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 30.0/86400.0 {
break
}
}
JD1 = JD1 - 15.0/86400.0
min := JD1
minRa := 100.0
for i := 0.0; i < 60.0; i++ {
tmp := decSub(JD1+i*0.5/86400.0, 0.5/86400.0)
if math.Abs(tmp) < math.Abs(minRa) {
minRa = tmp
min = JD1 + i*0.5/86400.0
}
}
//fmt.Println((min - lastHe) / (nextHe - lastHe))
return TD2UT(min, false)
}
func NextMercuryRetrograde(jde float64) float64 {
date := mercuryRetrograde(jde)
if date < jde {
nextHe := NextMercuryConjunction(jde)
return mercuryRetrograde(nextHe + 2)
}
return date
}
func LastMercuryRetrograde(jde float64) float64 {
lastHe := LastMercuryConjunction(jde)
date := mercuryRetrograde(lastHe + 2)
if date > jde {
lastLastHe := LastMercuryConjunction(lastHe - 2)
return mercuryRetrograde(lastLastHe + 2)
}
return date
}
func NextMercuryProgradeToRetrograde(jde float64) float64 {
date := NextMercuryRetrograde(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return NextMercuryRetrograde(date + MERCURY_S_PERIOD/2)
}
return date
}
func NextMercuryRetrogradeToPrograde(jde float64) float64 {
date := NextMercuryRetrograde(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return NextMercuryRetrograde(date + 12)
}
return date
}
func LastMercuryProgradeToRetrograde(jde float64) float64 {
date := LastMercuryRetrograde(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return LastMercuryRetrograde(date - 12)
}
return date
}
func LastMercuryRetrogradeToPrograde(jde float64) float64 {
date := LastMercuryRetrograde(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return LastMercuryRetrograde(date - MERCURY_S_PERIOD/2)
}
return date
}
func MercurySunElongation(jde float64) float64 {
lo1, bo1 := MercuryApparentLoBo(jde)
lo2 := SunApparentLo(jde)
bo2 := HSunTrueBo(jde)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
func mercuryGreatestElongation(jde float64) float64 {
decSunSub := func(jde float64) float64 {
sub := Limit360(MercuryApparentRa(jde) - SunApparentRa(jde))
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub
}
decSub := func(jde float64, val float64) float64 {
sub := MercurySunElongation(jde+val) - MercurySunElongation(jde-val)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * val)
}
lastHe := LastMercuryConjunction(jde)
nextHe := NextMercuryConjunction(jde)
nowSub := decSunSub(jde)
if nowSub > 0 {
jde = lastHe + ((nextHe - lastHe) / 5.0 * 2.0)
} else {
jde = lastHe + ((nextHe - lastHe) / 6.0)
}
for {
nowSub := decSub(jde, 1.0/86400.0)
if math.Abs(nowSub) > 0.4 {
jde += 2
continue
}
break
}
JD1 := jde
for {
JD0 := JD1
stDegree := decSub(JD0, 2.0/86400.0)
stDegreep := (decSub(JD0+15.0/86400.0, 2.0/86400.0) - decSub(JD0-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 30.0/86400.0 {
break
}
}
JD1 = JD1 - 15.0/86400.0
min := JD1
minRa := 100.0
for i := 0.0; i < 60.0; i++ {
tmp := decSub(JD1+i*0.5/86400.0, 0.5/86400.0)
if math.Abs(tmp) < math.Abs(minRa) {
minRa = tmp
min = JD1 + i*0.5/86400.0
}
}
//fmt.Println((min - lastHe) / (nextHe - lastHe))
return TD2UT(min, false)
}
func NextMercuryGreatestElongation(jde float64) float64 {
date := mercuryGreatestElongation(jde)
if date < jde {
nextHe := NextMercuryConjunction(jde)
return mercuryGreatestElongation(nextHe + 2)
}
return date
}
func LastMercuryGreatestElongation(jde float64) float64 {
lastHe := LastMercuryConjunction(jde)
date := mercuryGreatestElongation(lastHe + 2)
if date > jde {
lastLastHe := LastMercuryConjunction(lastHe - 2)
return mercuryGreatestElongation(lastLastHe + 2)
}
return date
}
func NextMercuryGreatestElongationEast(jde float64) float64 {
date := NextMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return NextMercuryGreatestElongation(date + 1)
}
return date
}
func NextMercuryGreatestElongationWest(jde float64) float64 {
date := NextMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return NextMercuryGreatestElongation(date + 1)
}
return date
}
func LastMercuryGreatestElongationEast(jde float64) float64 {
date := LastMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return LastMercuryGreatestElongation(date - 1)
}
return date
}
func LastMercuryGreatestElongationWest(jde float64) float64 {
date := LastMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return LastMercuryGreatestElongation(date - 1)
}
return date
func mercuryRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool) (float64, error) {
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, MercuryCulminationTime, MercuryHeight, MercuryApparentDec)
}
+51
View File
@@ -0,0 +1,51 @@
package basic
import "testing"
func BenchmarkMercuryConjunctionFamily(b *testing.B) {
cases := mercuryEventCases()[:6]
samples := mercuryEventSamples()
var sink float64
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, sample := range samples {
jd := mercuryEventSampleTTJD(sample)
for _, event := range cases {
sink += event.fn(jd)
}
}
}
_ = sink
}
func BenchmarkMercuryRetrogradeFamily(b *testing.B) {
cases := mercuryEventCases()[6:12]
samples := mercuryEventSamples()
var sink float64
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, sample := range samples {
jd := mercuryEventSampleTTJD(sample)
for _, event := range cases {
sink += event.fn(jd)
}
}
}
_ = sink
}
func BenchmarkMercuryGreatestElongationFamily(b *testing.B) {
cases := mercuryEventCases()[12:]
samples := mercuryEventSamples()
var sink float64
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, sample := range samples {
jd := mercuryEventSampleTTJD(sample)
for _, event := range cases {
sink += event.fn(jd)
}
}
}
_ = sink
}
+66
View File
@@ -0,0 +1,66 @@
package basic
import (
"math"
"time"
)
type mercuryEventFunc func(float64) float64
type mercuryEventCase struct {
name string
tolerance float64
fn mercuryEventFunc
}
func mercuryEventSamples() []time.Time {
start := time.Date(1995, 1, 15, 12, 34, 56, 789000000, time.UTC)
samples := make([]time.Time, 0, 96)
for i := 0; i < 48; i++ {
d := start.AddDate(0, 0, i*137)
d = d.Add(time.Duration((i%7)*3)*time.Hour + time.Duration((i%11)*7)*time.Minute + time.Duration((i%13)*11)*time.Second)
samples = append(samples, d)
}
extraStart := start.AddDate(0, 0, 41)
for i := 0; i < 48; i++ {
d := extraStart.AddDate(0, 0, i*89)
d = d.Add(time.Duration((i%5)*7)*time.Hour + time.Duration((i%13)*5)*time.Minute + time.Duration((i%17)*19)*time.Second)
samples = append(samples, d)
}
return samples
}
func mercuryEventSampleTTJD(date time.Time) float64 {
return TD2UT(Date2JDE(date.UTC()), true)
}
func mercuryEventCases() []mercuryEventCase {
const (
conjunctionTolerance = 0.00001
searchTolerance = 30.0 / 86400.0
)
return []mercuryEventCase{
{name: "LastMercuryConjunction", tolerance: conjunctionTolerance, fn: LastMercuryConjunction},
{name: "NextMercuryConjunction", tolerance: conjunctionTolerance, fn: NextMercuryConjunction},
{name: "LastMercuryInferiorConjunction", tolerance: conjunctionTolerance, fn: LastMercuryInferiorConjunction},
{name: "NextMercuryInferiorConjunction", tolerance: conjunctionTolerance, fn: NextMercuryInferiorConjunction},
{name: "LastMercurySuperiorConjunction", tolerance: conjunctionTolerance, fn: LastMercurySuperiorConjunction},
{name: "NextMercurySuperiorConjunction", tolerance: conjunctionTolerance, fn: NextMercurySuperiorConjunction},
{name: "LastMercuryRetrograde", tolerance: searchTolerance, fn: LastMercuryRetrograde},
{name: "NextMercuryRetrograde", tolerance: searchTolerance, fn: NextMercuryRetrograde},
{name: "LastMercuryProgradeToRetrograde", tolerance: searchTolerance, fn: LastMercuryProgradeToRetrograde},
{name: "NextMercuryProgradeToRetrograde", tolerance: searchTolerance, fn: NextMercuryProgradeToRetrograde},
{name: "LastMercuryRetrogradeToPrograde", tolerance: searchTolerance, fn: LastMercuryRetrogradeToPrograde},
{name: "NextMercuryRetrogradeToPrograde", tolerance: searchTolerance, fn: NextMercuryRetrogradeToPrograde},
{name: "LastMercuryGreatestElongation", tolerance: searchTolerance, fn: LastMercuryGreatestElongation},
{name: "NextMercuryGreatestElongation", tolerance: searchTolerance, fn: NextMercuryGreatestElongation},
{name: "LastMercuryGreatestElongationEast", tolerance: searchTolerance, fn: LastMercuryGreatestElongationEast},
{name: "NextMercuryGreatestElongationEast", tolerance: searchTolerance, fn: NextMercuryGreatestElongationEast},
{name: "LastMercuryGreatestElongationWest", tolerance: searchTolerance, fn: LastMercuryGreatestElongationWest},
{name: "NextMercuryGreatestElongationWest", tolerance: searchTolerance, fn: NextMercuryGreatestElongationWest},
}
}
func almostEqualWithinDays(got, want, tolerance float64) bool {
return math.Abs(got-want) <= tolerance
}
+57
View File
@@ -0,0 +1,57 @@
package basic
import (
"encoding/json"
"math"
"os"
"testing"
)
type mercuryEventBaselineSample struct {
InputUTC string `json:"input_utc"`
TTJDBits uint64 `json:"tt_jd_bits"`
Events map[string]uint64 `json:"events"`
}
type mercuryEventBaseline struct {
Samples []mercuryEventBaselineSample `json:"samples"`
}
func loadMercuryEventBaseline(t *testing.T) mercuryEventBaseline {
t.Helper()
data, err := os.ReadFile("testdata/mercury_event_baseline.json")
if err != nil {
t.Fatal(err)
}
var baseline mercuryEventBaseline
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatal(err)
}
if len(baseline.Samples) == 0 {
t.Fatal("empty mercury event baseline")
}
return baseline
}
func TestMercuryEventBaselineRegression(t *testing.T) {
baseline := loadMercuryEventBaseline(t)
cases := mercuryEventCases()
for _, sample := range baseline.Samples {
jd := math.Float64frombits(sample.TTJDBits)
for _, event := range cases {
wantBits, ok := sample.Events[event.name]
if !ok {
t.Fatalf("%s missing baseline event %s", sample.InputUTC, event.name)
}
want := math.Float64frombits(wantBits)
got := event.fn(jd)
diff := math.Abs(got - want)
if diff > event.tolerance {
t.Fatalf("%s %s diff %.12f > tolerance %.12f", sample.InputUTC, event.name, diff, event.tolerance)
}
}
}
}
+650
View File
@@ -0,0 +1,650 @@
package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
const (
MERCURY_S_PERIOD = 1 / ((1 / 87.9691) - (1 / 365.256363004))
mercuryConjunctionDerivativeStepDay = 2e-5 * 36525.0
mercuryLightTimeDaysPerAU = 0.0057755183
mercuryEventSearchN = 16
)
type mercuryConjunctionLBR struct {
lo float64
bo float64
r float64
}
type mercuryConjunctionGeo struct {
lo float64
bo float64
dist float64
}
type mercuryConjunctionResult struct {
diff float64
sunLightDays float64
geoLightDays float64
}
func mercuryHelioN(planetIndex int, jd float64, n int) mercuryConjunctionLBR {
return mercuryConjunctionLBR{
lo: planet.WherePlanetN(planetIndex, 0, jd, n),
bo: planet.WherePlanetN(planetIndex, 1, jd, n),
r: planet.WherePlanetN(planetIndex, 2, jd, n),
}
}
func mercuryGeocentric(planetPos, earthPos mercuryConjunctionLBR) mercuryConjunctionGeo {
x := planetPos.r*Cos(planetPos.bo)*Cos(planetPos.lo) - earthPos.r*Cos(earthPos.bo)*Cos(earthPos.lo)
y := planetPos.r*Cos(planetPos.bo)*Sin(planetPos.lo) - earthPos.r*Cos(earthPos.bo)*Sin(earthPos.lo)
z := planetPos.r*Sin(planetPos.bo) - earthPos.r*Sin(earthPos.bo)
dist := math.Sqrt(x*x + y*y + z*z)
return mercuryConjunctionGeo{
lo: Limit360(math.Atan2(y, x) * 180 / math.Pi),
bo: math.Atan2(z, math.Sqrt(x*x+y*y)) * 180 / math.Pi,
dist: dist,
}
}
func mercuryConjunctionAngleDelta(diff float64) float64 {
diff = Limit360(diff)
if diff > 180 {
diff -= 360
}
if diff < -180 {
diff += 360
}
return diff
}
func mercuryConjunctionHeliocentricDelta(jd, targetDeg float64, n int) float64 {
planetLo := planet.WherePlanetN(1, 0, jd, n)
earthLo := planet.WherePlanetN(-1, 0, jd, n)
return mercuryConjunctionAngleDelta(planetLo - earthLo - targetDeg)
}
func mercuryConjunctionDifference(jd float64, n int, targetDeg, sunLightDays, geoLightDays float64) mercuryConjunctionResult {
earthForSun := mercuryHelioN(-1, jd-sunLightDays, n)
sunLo := Limit360(earthForSun.lo + 180)
earth := mercuryHelioN(-1, jd-geoLightDays, n)
planetPos := mercuryHelioN(1, jd-geoLightDays, n)
geo := mercuryGeocentric(planetPos, earth)
return mercuryConjunctionResult{
diff: mercuryConjunctionAngleDelta(geo.lo - sunLo - targetDeg),
sunLightDays: earthForSun.r * mercuryLightTimeDaysPerAU,
geoLightDays: geo.dist * mercuryLightTimeDaysPerAU,
}
}
func mercuryConjunctionExactDelta(jd float64) float64 {
return mercuryConjunctionAngleDelta(MercuryApparentLo(jd) - HSunApparentLo(jd))
}
func mercuryConjunctionApproxTT(seed float64, inferior bool) float64 {
heliocentricTarget := 180.0
if inferior {
heliocentricTarget = 0
}
jd := seed
for i := 0; i < 6; i++ {
jd -= mercuryConjunctionHeliocentricDelta(jd, heliocentricTarget, 8) / (360.0 / MERCURY_S_PERIOD)
}
startSample := mercuryConjunctionDifference(jd, 8, 0, 0, 0)
nextSample := mercuryConjunctionDifference(jd+mercuryConjunctionDerivativeStepDay, 8, 0, 0, 0)
diffSlope := mercuryConjunctionAngleDelta(nextSample.diff-startSample.diff) / mercuryConjunctionDerivativeStepDay
refined := mercuryConjunctionDifference(jd, 40, 0, startSample.sunLightDays, startSample.geoLightDays)
jd -= refined.diff / diffSlope
final := mercuryConjunctionDifference(jd, -1, 0, refined.sunLightDays, refined.geoLightDays)
jd -= final.diff / diffSlope
return jd
}
func mercuryConjunctionExactTT(seed float64, inferior bool) float64 {
estimateJD := mercuryConjunctionApproxTT(seed, inferior)
for {
prevJD := estimateJD
longitudeDelta := mercuryConjunctionExactDelta(prevJD)
longitudeSlope := (mercuryConjunctionExactDelta(prevJD+0.000005) - mercuryConjunctionExactDelta(prevJD-0.000005)) / 0.00001
estimateJD = prevJD - longitudeDelta/longitudeSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return estimateJD
}
func mercuryConjunctionLegacy(jde float64, next uint8) float64 {
//0=last 1=next
longitudeDeltaAt := func(jde float64) float64 {
return mercuryConjunctionExactDelta(jde)
}
currentDelta := longitudeDeltaAt(jde)
distanceTrend := math.Abs(longitudeDeltaAt(jde+1/86400.0)) - math.Abs(currentDelta)
if distanceTrend >= 0 && next == 1 && currentDelta > 0 {
jde += MERCURY_S_PERIOD/8.0 + 2
}
if distanceTrend >= 0 && next == 1 && currentDelta < 0 {
jde += MERCURY_S_PERIOD/6.0 + 2
}
if distanceTrend <= 0 && next == 0 && currentDelta < 0 {
jde -= MERCURY_S_PERIOD/8.0 + 2
}
if distanceTrend <= 0 && next == 0 && currentDelta > 0 {
jde -= MERCURY_S_PERIOD/6.0 + 2
}
for {
currentDelta := longitudeDeltaAt(jde)
distanceTrend := math.Abs(longitudeDeltaAt(jde+1/86400.0)) - math.Abs(currentDelta)
if math.Abs(currentDelta) > 12 || (distanceTrend > 0 && next == 1) || (distanceTrend < 0 && next == 0) {
if next == 1 {
jde += 2
} else {
jde -= 2
}
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
longitudeDelta := longitudeDeltaAt(prevJD)
longitudeSlope := (longitudeDeltaAt(prevJD+0.000005) - longitudeDeltaAt(prevJD-0.000005)) / 0.00001
estimateJD = prevJD - longitudeDelta/longitudeSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return TD2UT(estimateJD, false)
}
func mercuryConjunction(jde float64, next uint8) float64 {
//0=last 1=next
currentDelta := mercuryConjunctionExactDelta(jde)
// pos 大于0:远离太阳 小于0:靠近太阳
distanceTrend := math.Abs(mercuryConjunctionExactDelta(jde+1/86400.0)) - math.Abs(currentDelta)
if distanceTrend >= 0 && next == 1 && currentDelta > 0 {
jde += MERCURY_S_PERIOD/8.0 + 2
}
if distanceTrend >= 0 && next == 1 && currentDelta < 0 {
jde += MERCURY_S_PERIOD/6.0 + 2
}
if distanceTrend <= 0 && next == 0 && currentDelta < 0 {
jde -= MERCURY_S_PERIOD/8.0 + 2
}
if distanceTrend <= 0 && next == 0 && currentDelta > 0 {
jde -= MERCURY_S_PERIOD/6.0 + 2
}
for {
currentDelta := mercuryConjunctionExactDelta(jde)
distanceTrend := math.Abs(mercuryConjunctionExactDelta(jde+1/86400.0)) - math.Abs(currentDelta)
if math.Abs(currentDelta) > 12 || (distanceTrend > 0 && next == 1) || (distanceTrend < 0 && next == 0) {
if next == 1 {
jde += 2
} else {
jde -= 2
}
continue
}
break
}
inferior := mercuryConjunctionExactTT(jde, true)
superior := mercuryConjunctionExactTT(jde, false)
best := inferior
if math.Abs(superior-jde) < math.Abs(inferior-jde) {
best = superior
}
return TD2UT(best, false)
}
func LastMercuryConjunction(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryConjunctionStrict, NextMercuryConjunctionStrict)
}
func NextMercuryConjunction(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryConjunctionStrict, NextMercuryConjunctionStrict)
}
func LastMercuryConjunctionStrict(jde float64) float64 {
return mercuryConjunction(jde, 0)
}
func NextMercuryConjunctionStrict(jde float64) float64 {
return mercuryConjunction(jde, 1)
}
func NextMercuryInferiorConjunction(jde float64) float64 {
date := NextMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) > EarthAway(date) {
return NextMercuryConjunctionStrict(date + 2)
}
return date
}
func NextMercurySuperiorConjunction(jde float64) float64 {
date := NextMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) < EarthAway(date) {
return NextMercuryConjunctionStrict(date + 2)
}
return date
}
func LastMercuryInferiorConjunction(jde float64) float64 {
date := LastMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) > EarthAway(date) {
return LastMercuryConjunctionStrict(date - 2)
}
return date
}
func LastMercurySuperiorConjunction(jde float64) float64 {
date := LastMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) < EarthAway(date) {
return LastMercuryConjunctionStrict(date - 2)
}
return date
}
func mercuryRetrograde(jde float64) float64 {
//0=last 1=next
solarRADelta := func(jde float64) float64 {
sub := Limit360(MercuryApparentRa(jde) - SunApparentRa(jde))
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub
}
lastConjunction := mercuryConjunctionLegacy(jde, 0)
nextConjunction := mercuryConjunctionLegacy(jde, 1)
currentRADelta := solarRADelta(jde)
if currentRADelta > 0 {
jde = lastConjunction + ((nextConjunction - lastConjunction) / 5.0 * 3.5)
} else {
jde = lastConjunction + ((nextConjunction - lastConjunction) / 5.5)
}
for {
currentRate := mercuryRADerivative(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
rateValue := mercuryRADerivative(prevJD, 2.0/86400.0)
rateSlope := (mercuryRADerivative(prevJD+15.0/86400.0, 2.0/86400.0) - mercuryRADerivative(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return mercuryRADerivative(jd, 0.5/86400.0)
})
//fmt.Println((bestJD - lastConjunction) / (nextConjunction - lastConjunction))
return TD2UT(bestJD, false)
}
func mercuryRADerivative(jde, delta float64) float64 {
sub := MercuryApparentRa(jde+delta) - MercuryApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func mercuryStationIsProgradeToRetrograde(eventUT float64) bool {
for _, offset := range []float64{0.25, 0.5, 1.0} {
before := mercuryRADerivative(eventUT-offset, 0.5/86400.0)
after := mercuryRADerivative(eventUT+offset, 0.5/86400.0)
if before > 0 && after < 0 {
return true
}
if before < 0 && after > 0 {
return false
}
}
before := mercuryRADerivative(eventUT-0.25, 0.5/86400.0)
after := mercuryRADerivative(eventUT+0.25, 0.5/86400.0)
return before > after
}
func nextMercuryTypedStation(jde float64, progradeToRetrograde bool) float64 {
date := NextMercuryRetrogradeStrict(jde)
for mercuryStationIsProgradeToRetrograde(date) != progradeToRetrograde {
date = NextMercuryRetrogradeStrict(eventUTNextQueryTT(date))
}
return date
}
func lastMercuryTypedStation(jde float64, progradeToRetrograde bool) float64 {
date := LastMercuryRetrogradeStrict(jde)
for mercuryStationIsProgradeToRetrograde(date) != progradeToRetrograde {
date = LastMercuryRetrogradeStrict(eventUTLastQueryTT(date))
}
return date
}
func NextMercuryRetrograde(jde float64) float64 {
date := mercuryRetrograde(jde)
if !eventUTQueryAfterOrEqual(date, jde) {
nextConjunction := NextMercuryConjunctionStrict(jde)
return mercuryRetrograde(nextConjunction + 2)
}
return date
}
func LastMercuryRetrograde(jde float64) float64 {
lastConjunction := LastMercuryConjunctionStrict(jde)
date := mercuryRetrograde(lastConjunction + 2)
if !eventUTQueryBeforeOrEqual(date, jde) {
previousConjunction := LastMercuryConjunctionStrict(eventUTLastQueryTT(lastConjunction))
return mercuryRetrograde(previousConjunction + 2)
}
return date
}
func LastMercuryRetrogradeStrict(jde float64) float64 {
return LastMercuryRetrograde(jde)
}
func NextMercuryRetrogradeStrict(jde float64) float64 {
return NextMercuryRetrograde(jde)
}
func NextMercuryProgradeToRetrograde(jde float64) float64 {
return nextMercuryTypedStation(jde, true)
}
func NextMercuryRetrogradeToPrograde(jde float64) float64 {
return nextMercuryTypedStation(jde, false)
}
func LastMercuryProgradeToRetrograde(jde float64) float64 {
return lastMercuryTypedStation(jde, true)
}
func LastMercuryRetrogradeToPrograde(jde float64) float64 {
return lastMercuryTypedStation(jde, false)
}
func MercurySunElongation(jde float64) float64 {
lo1, bo1 := MercuryApparentLoBo(jde)
lo2 := HSunApparentLo(jde)
bo2 := HSunTrueBo(jde)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
func mercurySunElongationN(jde float64, n int) float64 {
lo1, bo1 := MercuryApparentLoBoN(jde, n)
lo2 := HSunApparentLoN(jde, n)
bo2 := HSunTrueBoN(jde, n)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
func mercuryTrueElongationN(jde float64, n int) float64 {
earth := mercuryHelioN(-1, jde, n)
planetPos := mercuryHelioN(1, jde, n)
geo := mercuryGeocentric(planetPos, earth)
return StarAngularSeparation(geo.lo, geo.bo, HSunTrueLoN(jde, n), HSunTrueBoN(jde, n))
}
func mercuryGreatestElongationInWindow(start, end float64) float64 {
best := maximizeInWindow(start, end, 2.0, func(jd float64) float64 {
return mercuryTrueElongationN(jd, mercuryEventSearchN)
}, func(jd float64) float64 {
return mercuryTrueElongationN(jd, -1)
})
return TD2UT(best, false)
}
func mercuryEastElongationWindowEndingAt(inferior float64) (float64, float64) {
lastSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(inferior))
return lastSuperior + innerEventEpsilon, inferior - innerEventEpsilon
}
func mercuryWestElongationWindowEndingAt(superior float64) (float64, float64) {
lastInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(superior))
return lastInferior + innerEventEpsilon, superior - innerEventEpsilon
}
func mercuryEastElongationWindowContaining(jde float64) (float64, float64) {
nextInferior := NextMercuryInferiorConjunction(jde)
start, end := mercuryEastElongationWindowEndingAt(nextInferior)
if eventUTQueryBeforeOrEqual(start, jde) {
return start, end
}
currentInferior := LastMercuryInferiorConjunction(jde)
return mercuryEastElongationWindowEndingAt(currentInferior)
}
func mercuryWestElongationWindowContaining(jde float64) (float64, float64) {
nextSuperior := NextMercurySuperiorConjunction(jde)
start, end := mercuryWestElongationWindowEndingAt(nextSuperior)
if eventUTQueryBeforeOrEqual(start, jde) {
return start, end
}
currentSuperior := LastMercurySuperiorConjunction(jde)
return mercuryWestElongationWindowEndingAt(currentSuperior)
}
func nextMercuryGreatestElongationTyped(jde float64, east bool) float64 {
if east {
start, windowEnd := mercuryEastElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextInferior := NextMercuryInferiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = mercuryEastElongationWindowEndingAt(nextInferior)
}
}
start, windowEnd := mercuryWestElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextSuperior := NextMercurySuperiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = mercuryWestElongationWindowEndingAt(nextSuperior)
}
}
func lastMercuryGreatestElongationTyped(jde float64, east bool) float64 {
if east {
start, windowEnd := mercuryEastElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = mercuryEastElongationWindowEndingAt(prevInferior)
}
}
start, windowEnd := mercuryWestElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = mercuryWestElongationWindowEndingAt(prevSuperior)
}
}
func mercuryGreatestElongation(jde float64) float64 {
solarRADelta := func(jde float64) float64 {
sub := Limit360(MercuryApparentRa(jde) - SunApparentRa(jde))
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub
}
elongationRate := func(jde float64, delta float64) float64 {
sub := MercurySunElongation(jde+delta) - MercurySunElongation(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
lastConjunction := LastMercuryConjunctionStrict(jde)
nextConjunction := NextMercuryConjunctionStrict(jde)
currentRADelta := solarRADelta(jde)
if currentRADelta > 0 {
jde = lastConjunction + ((nextConjunction - lastConjunction) / 5.0 * 2.0)
} else {
jde = lastConjunction + ((nextConjunction - lastConjunction) / 6.0)
}
for {
currentRate := elongationRate(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.4 {
jde += 2
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
rateValue := elongationRate(prevJD, 2.0/86400.0)
rateSlope := (elongationRate(prevJD+15.0/86400.0, 2.0/86400.0) - elongationRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return elongationRate(jd, 0.5/86400.0)
})
//fmt.Println((bestJD - lastConjunction) / (nextConjunction - lastConjunction))
return TD2UT(bestJD, false)
}
func NextMercuryGreatestElongation(jde float64) float64 {
east := NextMercuryGreatestElongationEast(jde)
west := NextMercuryGreatestElongationWest(jde)
if sameEventJD(east, west) {
return east
}
if east < west {
return east
}
return west
}
func LastMercuryGreatestElongation(jde float64) float64 {
east := LastMercuryGreatestElongationEast(jde)
west := LastMercuryGreatestElongationWest(jde)
if sameEventJD(east, west) {
return east
}
if east > west {
return east
}
return west
}
func LastMercuryInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryInferiorConjunction, NextMercuryInferiorConjunction)
}
func NextMercuryInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryInferiorConjunction, NextMercuryInferiorConjunction)
}
func LastMercurySuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercurySuperiorConjunction, NextMercurySuperiorConjunction)
}
func NextMercurySuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercurySuperiorConjunction, NextMercurySuperiorConjunction)
}
func LastMercuryRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryRetrograde, NextMercuryRetrograde)
}
func NextMercuryRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryRetrograde, NextMercuryRetrograde)
}
func LastMercuryProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryProgradeToRetrograde, NextMercuryProgradeToRetrograde)
}
func NextMercuryProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryProgradeToRetrograde, NextMercuryProgradeToRetrograde)
}
func LastMercuryRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryRetrogradeToPrograde, NextMercuryRetrogradeToPrograde)
}
func NextMercuryRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryRetrogradeToPrograde, NextMercuryRetrogradeToPrograde)
}
func LastMercuryGreatestElongationInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongation, NextMercuryGreatestElongation)
}
func NextMercuryGreatestElongationInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongation, NextMercuryGreatestElongation)
}
func LastMercuryGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongationEast, NextMercuryGreatestElongationEast)
}
func NextMercuryGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongationEast, NextMercuryGreatestElongationEast)
}
func LastMercuryGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongationWest, NextMercuryGreatestElongationWest)
}
func NextMercuryGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongationWest, NextMercuryGreatestElongationWest)
}
func NextMercuryGreatestElongationEast(jde float64) float64 {
return nextMercuryGreatestElongationTyped(jde, true)
}
func NextMercuryGreatestElongationWest(jde float64) float64 {
return nextMercuryGreatestElongationTyped(jde, false)
}
func LastMercuryGreatestElongationEast(jde float64) float64 {
return lastMercuryGreatestElongationTyped(jde, true)
}
func LastMercuryGreatestElongationWest(jde float64) float64 {
return lastMercuryGreatestElongationTyped(jde, false)
}
+40
View File
@@ -0,0 +1,40 @@
package basic
import (
"math"
"testing"
"time"
)
func mercuryTTJDJST(year int, month time.Month, day, hour, minute, second int) float64 {
loc := time.FixedZone("JST", 9*3600)
return TD2UT(Date2JDE(time.Date(year, month, day, hour, minute, second, 0, loc).UTC()), true)
}
func TestMercuryTypedStationRegression1929(t *testing.T) {
loc := time.FixedZone("JST", 9*3600)
const tolerance = 30.0 / 86400.0
query := mercuryTTJDJST(1929, time.September, 20, 0, 0, 0)
wantP2R := mercuryTTJDJST(1929, time.September, 26, 1, 58, 0)
wantR2P := mercuryTTJDJST(1929, time.October, 16, 23, 32, 33)
nextP2R := NextMercuryProgradeToRetrograde(query)
nextR2P := NextMercuryRetrogradeToPrograde(query)
if math.Abs(nextP2R-wantP2R) > tolerance {
t.Fatalf("next P2R mismatch: got %s want %s", JDE2DateByZone(nextP2R, loc, false), JDE2DateByZone(wantP2R, loc, false))
}
if math.Abs(nextR2P-wantR2P) > tolerance {
t.Fatalf("next R2P mismatch: got %s want %s", JDE2DateByZone(nextR2P, loc, false), JDE2DateByZone(wantR2P, loc, false))
}
query = mercuryTTJDJST(1929, time.October, 20, 0, 0, 0)
lastP2R := LastMercuryProgradeToRetrograde(query)
lastR2P := LastMercuryRetrogradeToPrograde(query)
if math.Abs(lastP2R-wantP2R) > tolerance {
t.Fatalf("last P2R mismatch: got %s want %s", JDE2DateByZone(lastP2R, loc, false), JDE2DateByZone(wantP2R, loc, false))
}
if math.Abs(lastR2P-wantR2P) > tolerance {
t.Fatalf("last R2P mismatch: got %s want %s", JDE2DateByZone(lastR2P, loc, false), JDE2DateByZone(wantR2P, loc, false))
}
}
-16
View File
@@ -1,16 +0,0 @@
package basic
import (
"fmt"
"testing"
)
func TestMercury(t *testing.T) {
jde := GetNowJDE()
fmt.Println(2.459941309513889e+06, JDE2Date(2.459941309513889e+06), JDE2Date(8.0/24.0+LastMercuryGreatestElongation(2.459941309513889e+06)))
fmt.Println("-------------for------------")
for i := 0.00; i < 700.0; i += 5 {
fmt.Println(jde+i, JDE2Date(jde+i), JDE2Date(8.0/24.0+LastMercuryGreatestElongationWest(jde+i)))
// fmt.Println("")
}
}
+47 -1730
View File
File diff suppressed because one or more lines are too long
+45
View File
@@ -0,0 +1,45 @@
package basic
import . "b612.me/astro/tools"
// MoonBrightLimbPositionAngle 月亮明亮边缘位置角 / position angle of the Moon's bright limb.
func MoonBrightLimbPositionAngle(jd float64) float64 {
return MoonBrightLimbPositionAngleN(jd, -1)
}
// MoonBrightLimbPositionAngleN 月亮明亮边缘位置角(截断版) / truncated position angle of the Moon's bright limb.
func MoonBrightLimbPositionAngleN(jd float64, n int) float64 {
sunRA, sunDec := HSunApparentRaDecN(jd, n)
moonRA, moonDec := HMoonTrueRaDecN(jd, n)
return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec)
}
// MoonTopocentricBrightLimbPositionAngle 月亮站心明亮边缘位置角 / topocentric position angle of the Moon's bright limb.
func MoonTopocentricBrightLimbPositionAngle(jd, observerLon, observerLat, height float64) float64 {
return MoonTopocentricBrightLimbPositionAngleN(jd, observerLon, observerLat, height, -1)
}
// MoonTopocentricBrightLimbPositionAngleN 月亮站心明亮边缘位置角(截断版) / truncated topocentric position angle of the Moon's bright limb.
func MoonTopocentricBrightLimbPositionAngleN(jd, observerLon, observerLat, height float64, n int) float64 {
sunRA, sunDec := sunTopocentricApparentRaDecN(jd, observerLon, observerLat, height, n)
moonRA, moonDec := moonTopocentricApparentRaDecN(jd, observerLon, observerLat, height, n)
return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec)
}
func moonTopocentricApparentRaDecN(jd, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA := HMoonTrueRaN(jd, n)
geocentricDec := HMoonTrueDecN(jd, n)
distanceAU := HMoonAwayN(jd, n) / moonPhysicalAstronomicalUnitKM
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TD2UT(jd, false), distanceAU, height)
}
func sunTopocentricApparentRaDecN(jd, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA, geocentricDec := HSunApparentRaDecN(jd, n)
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TD2UT(jd, false), EarthAwayN(jd, n), height)
}
func brightLimbPositionAngleFromRaDec(sunRA, sunDec, bodyRA, bodyDec float64) float64 {
y := Cos(sunDec) * Sin(sunRA-bodyRA)
x := Sin(sunDec)*Cos(bodyDec) - Cos(sunDec)*Sin(bodyDec)*Cos(sunRA-bodyRA)
return ArcTan2(y, x)
}
+31
View File
@@ -0,0 +1,31 @@
package basic
import (
"math"
"testing"
"time"
)
func TestMoonBrightLimbPositionAngleMeeusExample(t *testing.T) {
assertPlanetPhaseClose(t, "MoonBrightLimbPositionAngle", MoonBrightLimbPositionAngle(2448724.5), 285.0, 0.1)
}
func TestMoonBrightLimbPositionAngleNFullMatchesDefault(t *testing.T) {
jd := TD2UT(Date2JDE(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)), true)
got := MoonBrightLimbPositionAngle(jd)
gotN := MoonBrightLimbPositionAngleN(jd, -1)
if math.Float64bits(got) != math.Float64bits(gotN) {
t.Fatalf("MoonBrightLimbPositionAngle full-n mismatch: got %.18f want %.18f", got, gotN)
}
}
func TestMoonTopocentricBrightLimbPositionAngleSampleFiniteAndInRange(t *testing.T) {
jd := TD2UT(Date2JDE(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)), true)
got := MoonTopocentricBrightLimbPositionAngle(jd, 121.4737, 31.2304, 4)
assertFiniteRange(t, "MoonTopocentricBrightLimbPositionAngle", got, 0, 360, true)
if angleDiffAbs(got, MoonBrightLimbPositionAngle(jd)) == 0 {
t.Fatalf("expected topocentric bright limb angle to differ from geocentric value")
}
}
+289
View File
@@ -0,0 +1,289 @@
package basic
import (
"math"
"sort"
"time"
. "b612.me/astro/tools"
)
const (
moonMaxDeclinationMeanMonthDays = 27.321582247
moonMaxDeclinationBaseCycle = 1336.86
moonMaxDeclinationSearchSpan = 3
)
// DeclinationEvent 赤纬极值事件 / declination extremum event.
type DeclinationEvent struct {
// JDE 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JDE float64
// Declination 是该时刻月心地心赤纬,单位度 / geocentric lunar declination at the event, in degrees.
Declination float64
}
type moonMaxDeclinationCoefficients struct {
D0 float64
M0 float64
MP0 float64
F0 float64
JDE0 float64
sign float64
tc [44]float64
dc [37]float64
}
var moonMaxDeclinationNorthCoefficients = moonMaxDeclinationCoefficients{
D0: 152.2029,
M0: 14.8591,
MP0: 4.6881,
F0: 325.8867,
JDE0: 2451562.5897,
sign: 1,
tc: [44]float64{
0.8975, -0.4726, -0.1030, -0.0976, -0.0462, -0.0461, -0.0438, 0.0162,
-0.0157, 0.0145, 0.0136, -0.0095, -0.0091, -0.0089, 0.0075, -0.0068,
0.0061, -0.0047, -0.0043, -0.0040, -0.0037, 0.0031, 0.0030, -0.0029,
-0.0029, -0.0027, 0.0024, -0.0021, 0.0019, 0.0018, 0.0018, 0.0017,
0.0017, -0.0014, 0.0013, 0.0013, 0.0012, 0.0011, -0.0011, 0.0010,
0.0010, -0.0009, 0.0007, -0.0007,
},
dc: [37]float64{
5.1093, 0.2658, 0.1448, -0.0322, 0.0133, 0.0125, -0.0124, -0.0101,
0.0097, -0.0087, 0.0074, 0.0067, 0.0063, 0.0060, -0.0057, -0.0056,
0.0052, 0.0041, -0.0040, 0.0038, -0.0034, -0.0029, 0.0029, -0.0028,
-0.0028, -0.0023, -0.0021, 0.0019, 0.0018, 0.0017, 0.0015, 0.0014,
-0.0012, -0.0012, -0.0010, -0.0010, 0.0006,
},
}
var moonMaxDeclinationSouthCoefficients = moonMaxDeclinationCoefficients{
D0: 345.6676,
M0: 1.3951,
MP0: 186.21,
F0: 145.1633,
JDE0: 2451548.9289,
sign: -1,
tc: [44]float64{
-0.8975, -0.4726, -0.1030, -0.0976, 0.0541, 0.0516, -0.0438, 0.0112,
0.0157, 0.0023, -0.0136, 0.0110, 0.0091, 0.0089, 0.0075, -0.0030,
-0.0061, -0.0047, -0.0043, 0.0040, -0.0037, -0.0031, 0.0030, 0.0029,
-0.0029, -0.0027, 0.0024, -0.0021, -0.0019, -0.0006, -0.0018, -0.0017,
0.0017, 0.0014, -0.0013, -0.0013, 0.0012, 0.0011, 0.0011, 0.0010,
0.0010, -0.0009, -0.0007, -0.0007,
},
dc: [37]float64{
-5.1093, 0.2658, -0.1448, 0.0322, 0.0133, 0.0125, -0.0015, 0.0101,
-0.0097, 0.0087, 0.0074, 0.0067, -0.0063, -0.0060, 0.0057, -0.0056,
-0.0052, -0.0041, -0.0040, -0.0038, 0.0034, -0.0029, 0.0029, 0.0028,
-0.0028, 0.0023, 0.0021, 0.0019, 0.0018, -0.0017, 0.0015, 0.0014,
0.0012, -0.0012, 0.0010, -0.0010, 0.0037,
},
}
// MoonMaximumNorthDeclinations 指定年月内的所有月球最大北赤纬事件 / all maximum northern lunar declination events in the given Gregorian month.
func MoonMaximumNorthDeclinations(year int, month time.Month) []DeclinationEvent {
return moonMaximumDeclinationsInMonth(year, month, moonMaxDeclinationNorthCoefficients)
}
// MoonMaximumSouthDeclinations 指定年月内的所有月球最大南赤纬事件 / all maximum southern lunar declination events in the given Gregorian month.
func MoonMaximumSouthDeclinations(year int, month time.Month) []DeclinationEvent {
return moonMaximumDeclinationsInMonth(year, month, moonMaxDeclinationSouthCoefficients)
}
// LastMoonMaximumNorthDeclination 指定时刻之前最近一次月球最大北赤纬 / last maximum northern lunar declination at or before jd.
func LastMoonMaximumNorthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationNorthCoefficients, -1, true)
}
// NextMoonMaximumNorthDeclination 指定时刻之后最近一次月球最大北赤纬 / next maximum northern lunar declination after jd.
func NextMoonMaximumNorthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationNorthCoefficients, 1, false)
}
// ClosestMoonMaximumNorthDeclination 离指定时刻最近一次月球最大北赤纬 / closest maximum northern lunar declination to jd.
func ClosestMoonMaximumNorthDeclination(jd float64) DeclinationEvent {
return moonClosestMaximumDeclination(jd, moonMaxDeclinationNorthCoefficients)
}
// LastMoonMaximumSouthDeclination 指定时刻之前最近一次月球最大南赤纬 / last maximum southern lunar declination at or before jd.
func LastMoonMaximumSouthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationSouthCoefficients, -1, true)
}
// NextMoonMaximumSouthDeclination 指定时刻之后最近一次月球最大南赤纬 / next maximum southern lunar declination after jd.
func NextMoonMaximumSouthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationSouthCoefficients, 1, false)
}
// ClosestMoonMaximumSouthDeclination 离指定时刻最近一次月球最大南赤纬 / closest maximum southern lunar declination to jd.
func ClosestMoonMaximumSouthDeclination(jd float64) DeclinationEvent {
return moonClosestMaximumDeclination(jd, moonMaxDeclinationSouthCoefficients)
}
func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDeclinationCoefficients) []DeclinationEvent {
startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
endUTC := startUTC.AddDate(0, 1, 0)
startTT := TD2UT(Date2JDE(startUTC), true)
endTT := TD2UT(Date2JDE(endUTC), true)
kStart := int(math.Floor((startTT-coeffs.JDE0)/moonMaxDeclinationMeanMonthDays)) - 1
kEnd := int(math.Ceil((endTT-coeffs.JDE0)/moonMaxDeclinationMeanMonthDays)) + 1
cfg := apsisSearchConfig{
bracketHalfWidth: moonApsisBracketHalfWidth,
sampleStep: moonApsisSampleStep,
derivativeStep: moonApsisDerivativeStep,
toleranceDays: moonApsisToleranceDays,
maxIterations: moonApsisMaxIterations,
maximize: coeffs.sign > 0,
}
events := make([]DeclinationEvent, 0, 2)
for k := kStart; k <= kEnd; k++ {
event := moonMaximumDeclinationEvent(k, coeffs, cfg)
eventTimeUTC := JDE2DateByZone(event.JDE, time.UTC, false)
if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) {
continue
}
events = append(events, event)
}
sort.Slice(events, func(i, j int) bool {
return events[i].JDE < events[j].JDE
})
return events
}
func moonMaximumDeclinationEvent(k int, coeffs moonMaxDeclinationCoefficients, cfg apsisSearchConfig) DeclinationEvent {
seedTT := moonMaximumDeclinationSeedTT(k, coeffs)
eventTT, declination := refineDistanceExtremum(seedTT, cfg, func(sampleTT float64) float64 {
return HMoonTrueDecN(sampleTT, -1)
})
return DeclinationEvent{
JDE: TD2UT(eventTT, false),
Declination: declination,
}
}
func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficients, direction int, includeCurrent bool) DeclinationEvent {
cfg := apsisSearchConfig{
bracketHalfWidth: moonApsisBracketHalfWidth,
sampleStep: moonApsisSampleStep,
derivativeStep: moonApsisDerivativeStep,
toleranceDays: moonApsisToleranceDays,
maxIterations: moonApsisMaxIterations,
maximize: coeffs.sign > 0,
}
targetTT := TD2UT(jd, true)
centerK := int(math.Round((targetTT - coeffs.JDE0) / moonMaxDeclinationMeanMonthDays))
found := false
bestDistance := math.Inf(1)
var best DeclinationEvent
for offset := -moonMaxDeclinationSearchSpan; offset <= moonMaxDeclinationSearchSpan; offset++ {
event := moonMaximumDeclinationEvent(centerK+offset, coeffs, cfg)
delta := event.JDE - jd
if !moonMaximumDeclinationMatchesDirection(delta, direction, includeCurrent) {
continue
}
distance := math.Abs(delta)
if !found || distance < bestDistance || (distance == bestDistance && moonMaximumDeclinationEarlier(event, best)) {
best = event
bestDistance = distance
found = true
}
}
return best
}
func moonClosestMaximumDeclination(jd float64, coeffs moonMaxDeclinationCoefficients) DeclinationEvent {
last := moonMaximumDeclinationSearch(jd, coeffs, -1, true)
next := moonMaximumDeclinationSearch(jd, coeffs, 1, false)
lastDistance := math.Abs(jd - last.JDE)
nextDistance := math.Abs(next.JDE - jd)
if lastDistance <= nextDistance {
return last
}
return next
}
func moonMaximumDeclinationMatchesDirection(delta float64, direction int, includeCurrent bool) bool {
switch direction {
case -1:
if includeCurrent {
return delta <= 0
}
return delta < 0
case 1:
if includeCurrent {
return delta >= 0
}
return delta > 0
default:
return true
}
}
func moonMaximumDeclinationEarlier(a, b DeclinationEvent) bool {
return a.JDE < b.JDE
}
func moonMaximumDeclinationSeedTT(k int, coeffs moonMaxDeclinationCoefficients) float64 {
cycle := float64(k)
T := cycle / moonMaxDeclinationBaseCycle
D := Limit360(coeffs.D0 + 333.0705546*cycle - 0.0004214*T*T + 0.00000011*T*T*T)
M := Limit360(coeffs.M0 + 26.9281592*cycle - 0.0000355*T*T - 0.0000001*T*T*T)
MP := Limit360(coeffs.MP0 + 356.9562794*cycle + 0.0103066*T*T + 0.00001251*T*T*T)
F := Limit360(coeffs.F0 + 1.4467807*cycle - 0.0020690*T*T - 0.00000215*T*T*T)
E := 1 - 0.002516*T - 0.0000074*T*T
return coeffs.JDE0 +
moonMaxDeclinationMeanMonthDays*cycle +
0.000119804*T*T -
0.000000141*T*T*T +
coeffs.tc[0]*Cos(F) +
coeffs.tc[1]*Sin(MP) +
coeffs.tc[2]*Sin(2*F) +
coeffs.tc[3]*Sin(2*D-MP) +
coeffs.tc[4]*Cos(MP-F) +
coeffs.tc[5]*Cos(MP+F) +
coeffs.tc[6]*Sin(2*D) +
coeffs.tc[7]*Sin(M)*E +
coeffs.tc[8]*Cos(3*F) +
coeffs.tc[9]*Sin(MP+2*F) +
coeffs.tc[10]*Cos(2*D-F) +
coeffs.tc[11]*Cos(2*D-MP-F) +
coeffs.tc[12]*Cos(2*D-MP+F) +
coeffs.tc[13]*Cos(2*D+F) +
coeffs.tc[14]*Sin(2*MP) +
coeffs.tc[15]*Sin(MP-2*F) +
coeffs.tc[16]*Cos(2*MP-F) +
coeffs.tc[17]*Sin(MP+3*F) +
coeffs.tc[18]*Sin(2*D-M-MP)*E +
coeffs.tc[19]*Cos(MP-2*F) +
coeffs.tc[20]*Sin(2*(D-MP)) +
coeffs.tc[21]*Sin(F) +
coeffs.tc[22]*Sin(2*D+MP) +
coeffs.tc[23]*Cos(MP+2*F) +
coeffs.tc[24]*Sin(2*D-M)*E +
coeffs.tc[25]*Sin(MP+F) +
coeffs.tc[26]*Sin(M-MP)*E +
coeffs.tc[27]*Sin(MP-3*F) +
coeffs.tc[28]*Sin(2*MP+F) +
coeffs.tc[29]*Cos(2*(D-MP)-F) +
coeffs.tc[30]*Sin(3*F) +
coeffs.tc[31]*Cos(MP+3*F) +
coeffs.tc[32]*Cos(2*MP) +
coeffs.tc[33]*Cos(2*D-MP) +
coeffs.tc[34]*Cos(2*D+MP+F) +
coeffs.tc[35]*Cos(MP) +
coeffs.tc[36]*Sin(3*MP+F) +
coeffs.tc[37]*Sin(2*D-MP+F) +
coeffs.tc[38]*Cos(2*(D-MP)) +
coeffs.tc[39]*Cos(D+F) +
coeffs.tc[40]*Sin(M+MP)*E +
coeffs.tc[41]*Sin(2*(D-F)) +
coeffs.tc[42]*Cos(2*MP+F) +
coeffs.tc[43]*Cos(3*MP+F)
}
+213
View File
@@ -0,0 +1,213 @@
package basic
import (
"encoding/json"
"math"
"os"
"testing"
"time"
)
type moonMaxDeclinationSample struct {
Kind string `json:"kind"`
Year int `json:"year"`
Month int `json:"month"`
TimeUTC string `json:"time_utc"`
DeclinationDeg float64 `json:"declination_deg"`
}
type moonMaxDeclinationMonthState struct {
north []DeclinationEvent
south []DeclinationEvent
northI int
southI int
}
func TestMoonMaximumDeclinationsMatchHorizonsBaseline(t *testing.T) {
// Baseline is generated from JPL Horizons by scripts/generate_moon_max_declination_baseline.sh.
data, err := os.ReadFile("testdata/moon_max_declination_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []moonMaxDeclinationSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
if len(samples) == 0 {
t.Fatal("empty moon maximum declination baseline")
}
const timeTolerance = 15 * time.Second
const declinationToleranceDeg = 0.0002
states := make(map[int]*moonMaxDeclinationMonthState)
var maxTimeDiff time.Duration
var maxDeclinationDiff float64
for _, sample := range samples {
wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
}
key := sample.Year*100 + sample.Month
state := states[key]
if state == nil {
state = &moonMaxDeclinationMonthState{
north: MoonMaximumNorthDeclinations(sample.Year, time.Month(sample.Month)),
south: MoonMaximumSouthDeclinations(sample.Year, time.Month(sample.Month)),
}
states[key] = state
}
var got DeclinationEvent
switch sample.Kind {
case "north":
if state.northI >= len(state.north) {
t.Fatalf("%04d-%02d missing north declination event #%d", sample.Year, sample.Month, state.northI+1)
}
got = state.north[state.northI]
state.northI++
case "south":
if state.southI >= len(state.south) {
t.Fatalf("%04d-%02d missing south declination event #%d", sample.Year, sample.Month, state.southI+1)
}
got = state.south[state.southI]
state.southI++
default:
t.Fatalf("unknown declination kind %q", sample.Kind)
}
gotTime := JDE2DateByZone(got.JDE, time.UTC, false)
timeDiff := gotTime.Sub(wantTime)
if timeDiff < 0 {
timeDiff = -timeDiff
}
if timeDiff > maxTimeDiff {
maxTimeDiff = timeDiff
}
if timeDiff > timeTolerance {
t.Fatalf("%s %04d-%02d time mismatch: got %s want %s tolerance %v", sample.Kind, sample.Year, sample.Month, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, timeTolerance)
}
declinationDiff := math.Abs(got.Declination - sample.DeclinationDeg)
if declinationDiff > maxDeclinationDiff {
maxDeclinationDiff = declinationDiff
}
if declinationDiff > declinationToleranceDeg {
t.Fatalf("%s %04d-%02d declination mismatch: got %.8f want %.8f tolerance %.8f", sample.Kind, sample.Year, sample.Month, got.Declination, sample.DeclinationDeg, declinationToleranceDeg)
}
}
for key, state := range states {
year := key / 100
month := key % 100
if state.northI != len(state.north) {
t.Fatalf("%04d-%02d unconsumed north events: got %d of %d", year, month, state.northI, len(state.north))
}
if state.southI != len(state.south) {
t.Fatalf("%04d-%02d unconsumed south events: got %d of %d", year, month, state.southI, len(state.south))
}
}
t.Logf("moon maximum declination max diff: time=%v declination=%.8f deg", maxTimeDiff, maxDeclinationDiff)
}
func TestMoonMaximumDeclinationSignsAndOrder(t *testing.T) {
north := MoonMaximumNorthDeclinations(2026, time.January)
south := MoonMaximumSouthDeclinations(2026, time.January)
if len(north) == 0 || len(south) == 0 {
t.Fatalf("expected both north and south events in 2026-01, got north=%d south=%d", len(north), len(south))
}
for i, event := range north {
if event.Declination <= 0 {
t.Fatalf("north event #%d should be positive, got %.8f", i+1, event.Declination)
}
if i > 0 && !(north[i-1].JDE < event.JDE) {
t.Fatalf("north events not strictly increasing: %.12f then %.12f", north[i-1].JDE, event.JDE)
}
}
for i, event := range south {
if event.Declination >= 0 {
t.Fatalf("south event #%d should be negative, got %.8f", i+1, event.Declination)
}
if i > 0 && !(south[i-1].JDE < event.JDE) {
t.Fatalf("south events not strictly increasing: %.12f then %.12f", south[i-1].JDE, event.JDE)
}
}
}
func TestMoonMaximumDeclinationSearchMatchesMonthlyEvents(t *testing.T) {
query := time.Date(2026, time.January, 10, 0, 0, 0, 0, time.UTC)
queryJDE := Date2JDE(query)
northEvents := append([]DeclinationEvent{}, MoonMaximumNorthDeclinations(2025, time.December)...)
northEvents = append(northEvents, MoonMaximumNorthDeclinations(2026, time.January)...)
northEvents = append(northEvents, MoonMaximumNorthDeclinations(2026, time.February)...)
southEvents := append([]DeclinationEvent{}, MoonMaximumSouthDeclinations(2025, time.December)...)
southEvents = append(southEvents, MoonMaximumSouthDeclinations(2026, time.January)...)
southEvents = append(southEvents, MoonMaximumSouthDeclinations(2026, time.February)...)
assertSameDeclinationEvent(t, "last north", LastMoonMaximumNorthDeclination(queryJDE), expectedDirectionalDeclinationEvent(northEvents, queryJDE, -1, true))
assertSameDeclinationEvent(t, "next north", NextMoonMaximumNorthDeclination(queryJDE), expectedDirectionalDeclinationEvent(northEvents, queryJDE, 1, false))
assertSameDeclinationEvent(t, "closest north", ClosestMoonMaximumNorthDeclination(queryJDE), expectedClosestDeclinationEvent(northEvents, queryJDE))
assertSameDeclinationEvent(t, "last south", LastMoonMaximumSouthDeclination(queryJDE), expectedDirectionalDeclinationEvent(southEvents, queryJDE, -1, true))
assertSameDeclinationEvent(t, "next south", NextMoonMaximumSouthDeclination(queryJDE), expectedDirectionalDeclinationEvent(southEvents, queryJDE, 1, false))
assertSameDeclinationEvent(t, "closest south", ClosestMoonMaximumSouthDeclination(queryJDE), expectedClosestDeclinationEvent(southEvents, queryJDE))
}
func TestMoonMaximumDeclinationSearchAtExactEventTime(t *testing.T) {
north := MoonMaximumNorthDeclinations(2026, time.January)
if len(north) < 2 {
t.Fatalf("expected at least two north events spanning Jan 2026 search window, got %d", len(north))
}
exactJDE := north[0].JDE
assertSameDeclinationEvent(t, "exact last north", LastMoonMaximumNorthDeclination(exactJDE), north[0])
assertSameDeclinationEvent(t, "exact closest north", ClosestMoonMaximumNorthDeclination(exactJDE), north[0])
assertSameDeclinationEvent(t, "exact next north", NextMoonMaximumNorthDeclination(exactJDE), north[1])
}
func assertSameDeclinationEvent(t *testing.T, name string, got, want DeclinationEvent) {
t.Helper()
if math.Abs(got.JDE-want.JDE) > 1e-12 {
t.Fatalf("%s JDE mismatch: got %.12f want %.12f", name, got.JDE, want.JDE)
}
if math.Float64bits(got.Declination) != math.Float64bits(want.Declination) {
t.Fatalf("%s declination mismatch: got %.12f want %.12f", name, got.Declination, want.Declination)
}
}
func expectedDirectionalDeclinationEvent(events []DeclinationEvent, queryJDE float64, direction int, includeCurrent bool) DeclinationEvent {
var (
found bool
best DeclinationEvent
)
for _, event := range events {
delta := event.JDE - queryJDE
if !moonMaximumDeclinationMatchesDirection(delta, direction, includeCurrent) {
continue
}
if !found {
best = event
found = true
continue
}
if math.Abs(delta) < math.Abs(best.JDE-queryJDE) || (math.Abs(delta) == math.Abs(best.JDE-queryJDE) && event.JDE < best.JDE) {
best = event
}
}
return best
}
func expectedClosestDeclinationEvent(events []DeclinationEvent, queryJDE float64) DeclinationEvent {
last := expectedDirectionalDeclinationEvent(events, queryJDE, -1, true)
next := expectedDirectionalDeclinationEvent(events, queryJDE, 1, false)
if math.Abs(queryJDE-last.JDE) <= math.Abs(next.JDE-queryJDE) {
return last
}
return next
}
+347
View File
@@ -0,0 +1,347 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
/*
* 月球方位角
*/
func MoonAzimuth(jd, lon, lat, tz float64) float64 {
//tmp := (tz*15 - lon) * 4 / 60
calcjd := TD2UT(jd-tz/24, true)
ra := MoonTrueRa(calcjd)
dec := MoonTrueDec(calcjd)
away := MoonAway(calcjd) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
calcjd = jd - tz/24
st := Limit360(ApparentSiderealTime(calcjd)*15 + lon)
hourAngle := Limit360(st - nra)
tmp2 := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(ndec)*Cos(lat))
azimuth := ArcTan(tmp2)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
} else {
return azimuth + 180
}
} else {
if hourAngle/15 < 12 {
return azimuth + 180
} else {
return azimuth
}
}
}
func MoonHeight(jd, lon, lat, tz float64) float64 {
// tmp := (tz*15 - lon) * 4 / 60
//truejd=jd-tmp/24;
calcjd := TD2UT(jd-tz/24, true)
ra := MoonTrueRa(calcjd)
dec := MoonTrueDec(calcjd)
away := MoonAway(calcjd) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
calcjd = jd - tz/24
st := Limit360(ApparentSiderealTime(calcjd)*15 + lon)
hourAngle := Limit360(st - nra)
tmp2 := Sin(lat)*Sin(ndec) + Cos(ndec)*Cos(lat)*Cos(hourAngle)
return ArcSin(tmp2)
}
func HMoonAzimuth(jd, lon, lat, tz float64) float64 {
return HMoonAzimuthN(jd, lon, lat, tz, -1)
}
func HMoonAzimuthN(jd, lon, lat, tz float64, n int) float64 {
calcjd := TD2UT(jd-tz/24, true)
ra := HMoonTrueRaN(calcjd, n)
dec := HMoonTrueDecN(calcjd, n)
away := HMoonAwayN(calcjd, n) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
calcjd = jd - tz/24
st := Limit360(ApparentSiderealTime(calcjd)*15 + lon)
hourAngle := Limit360(st - nra)
tmp2 := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(ndec)*Cos(lat))
azimuth := ArcTan(tmp2)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
} else {
return azimuth + 180
}
} else {
if hourAngle/15 < 12 {
return azimuth + 180
} else {
return azimuth
}
}
}
func HMoonHeight(jd, lon, lat, tz float64) float64 {
return HMoonHeightN(jd, lon, lat, tz, -1)
}
func HMoonHeightN(jd, lon, lat, tz float64, n int) float64 {
calcjd := TD2UT(jd-tz/24, true)
ra, dec := HMoonTrueRaDecN(calcjd, n)
away := HMoonAwayN(calcjd, n) / 149597870.7
nra, ndec := TopocentricRaDec(ra, dec, lat, lon, calcjd, away, 0)
calcjd = jd - tz/24
st := Limit360(ApparentSiderealTime(calcjd)*15 + lon)
hourAngle := Limit360(st - nra)
tmp2 := Sin(lat)*Sin(ndec) + Cos(ndec)*Cos(lat)*Cos(hourAngle)
return ArcSin(tmp2)
}
// 废弃
func GetMoonTZTime(jd, lon, lat, tz float64) float64 { //实际中天时间{
jd = math.Floor(jd) + 0.5
ttm := MoonTimeAngle(jd, lon, lat, tz)
if ttm > 0 && ttm < 180 {
jd += 0.5
}
estimateJD := jd
for {
prevJD := estimateJD
stDegree := MoonTimeAngle(prevJD, lon, lat, tz) - 359.599
stDegreep := (MoonTimeAngle(prevJD+0.000005, lon, lat, tz) - MoonTimeAngle(prevJD-0.000005, lon, lat, tz)) / 0.00001
estimateJD = prevJD - stDegree/stDegreep
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return estimateJD
}
func MoonCulminationTime(jde, lon, lat, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5
estimateJD := jde + Limit360(360-MoonTimeAngle(jde, lon, lat, timezone))/15.0/24.0/0.9
limitHA := func(jde, lon, timezone float64) float64 {
ha := MoonTimeAngle(jde, lon, lat, timezone)
if ha < 180 {
ha += 360
}
return ha
}
for {
prevJD := estimateJD
stDegree := limitHA(prevJD, lon, timezone) - 360
stDegreep := (limitHA(prevJD+0.000005, lon, timezone) - limitHA(prevJD-0.000005, lon, timezone)) / 0.00001
estimateJD = prevJD - stDegree/stDegreep
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return estimateJD
}
func MoonTimeAngle(jd, lon, lat, tz float64) float64 {
startime := Limit360(ApparentSiderealTime(jd-tz/24)*15 + lon)
timeangle := startime - HMoonApparentRa(jd, lon, lat, tz)
if timeangle < 0 {
timeangle += 360
}
return timeangle
}
func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) {
originalTimeZone := timeZone
timeZone = longitude / 15
var timeToMeridian float64
julianDayZero := math.Floor(julianDay) + 0.5
//julianDay = math.Floor(julianDay) + 0.5 - originalTimeZone/24 + timeZone/24 // 求0时JDE
//fix:这里时间分界线应当以传入的时区为准,不应当使用当地时区,否则在0时的判断会出错
julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay
moonHeight := MoonHeight(julianDay, longitude, latitude, originalTimeZone) // 求此时月亮高度
moonAngle := StandardAltitudeMoon(zenithShift, height, latitude)
moonAngleTime := MoonTimeAngle(julianDay, longitude, latitude, originalTimeZone)
if moonHeight-moonAngle > 0 { // 月亮在地平线上或在落下与下中天之间
if moonAngleTime > 180 {
timeToMeridian = (180 + 360 - moonAngleTime) / 15
} else {
timeToMeridian = (180 - moonAngleTime) / 15
}
estimatedTime += (timeToMeridian/24 + (timeToMeridian/24*12.0)/15.0/24.0)
}
if moonHeight-moonAngle < 0 && moonAngleTime > 180 {
timeToMeridian = (180 - moonAngleTime) / 15
estimatedTime += (timeToMeridian/24 + (timeToMeridian/24*12.0)/15.0/24.0)
} else if moonHeight-moonAngle < 0 && moonAngleTime < 180 {
timeToMeridian = (180 - moonAngleTime) / 15
estimatedTime += (timeToMeridian/24 + (timeToMeridian/24*12.0)/15.0/24.0)
}
currentAngle := MoonTimeAngle(estimatedTime, longitude, latitude, timeZone)
if math.Abs(currentAngle-180) > 0.5 {
estimatedTime += (180 - currentAngle) * 4.0 / 60.0 / 24.0
}
currentHeight := HMoonHeight(estimatedTime, longitude, latitude, timeZone)
if !(currentHeight < -10 && math.Abs(latitude) < 60) {
if currentHeight > moonAngle {
return 0, ErrNeverSet
}
checkTime := estimatedTime + 12.0/24.0 + 6.0/15.0/24.0
checkAngle := MoonTimeAngle(checkTime, longitude, latitude, timeZone)
if checkAngle < 90 {
checkAngle += 360
}
checkTime += (360 - checkAngle) * 4.0 / 60.0 / 24.0
if HMoonHeight(checkTime, longitude, latitude, timeZone) < moonAngle {
return 0, ErrNeverRise
}
}
moonDeclination := MoonApparentDec(estimatedTime, longitude, latitude, timeZone)
tmp := (Sin(moonAngle) - Sin(moonDeclination)*Sin(latitude)) / (Cos(moonDeclination) * Cos(latitude))
if math.Abs(tmp) <= 1 && latitude < 85 {
hourAngle := (180 - ArcCos(tmp)) / 15
estimatedTime += hourAngle/24.00 + hourAngle/33.00/15.00
} else {
i := 0
for MoonHeight(estimatedTime, longitude, latitude, timeZone) < moonAngle {
i++
estimatedTime += 15.0 / 60.0 / 24.0
if i > 48 {
break
}
}
}
// 使用牛顿迭代法求精确解
estimatedTime = moonRiseSetNewtonRaphsonIteration(estimatedTime, longitude, latitude, timeZone, moonAngle, HMoonHeight, 0.00002)
estimatedTime = estimatedTime - timeZone/24 + originalTimeZone/24
if estimatedTime > julianDayZero+1 || estimatedTime < julianDayZero {
return 0, ErrNotOnThisDate
}
return estimatedTime, nil
}
func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) {
originalTimeZone := timeZone
timeZone = longitude / 15
var timeToMeridian float64
julianDayZero := math.Floor(julianDay) + 0.5
//julianDay = math.Floor(julianDay) + 0.5 - originalTimeZone/24 + timeZone/24 // 求0时JDE
//fix:这里时间分界线应当以传入的时区为准,不应当使用当地时区,否则在0时的判断会出错
julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay
moonHeight := MoonHeight(julianDay, longitude, latitude, originalTimeZone) // 求此时月亮高度
moonAngle := StandardAltitudeMoon(zenithShift, height, latitude)
moonAngleTime := MoonTimeAngle(julianDay, longitude, latitude, originalTimeZone)
if moonHeight-moonAngle < 0 {
timeToMeridian = (360 - moonAngleTime) / 15
estimatedTime += (timeToMeridian/24 + (timeToMeridian/24.0*12.0)/15.0/24.0)
}
// 月亮在地平线上或在落下与下中天之间
if moonHeight-moonAngle > 0 && moonAngleTime < 180 {
timeToMeridian = (-moonAngleTime) / 15
estimatedTime += (timeToMeridian/24.0 + (timeToMeridian/24.0*12.0)/15.0/24.0)
} else if moonHeight-moonAngle > 0 {
timeToMeridian = (360 - moonAngleTime) / 15
estimatedTime += (timeToMeridian/24.0 + (timeToMeridian/24.0*12.0)/15.0/24.0)
}
currentAngle := MoonTimeAngle(estimatedTime, longitude, latitude, timeZone)
if currentAngle < 180 {
currentAngle += 360
}
if math.Abs(currentAngle-360) > 0.5 {
estimatedTime += (360 - currentAngle) * 4.0 / 60.0 / 24.0
}
// estimatedTime = 月球中天时间
currentHeight := HMoonHeight(estimatedTime, longitude, latitude, timeZone)
if !(currentHeight > 10 && math.Abs(latitude) < 60) {
if currentHeight < moonAngle {
return 0, ErrNeverRise
}
checkTime := estimatedTime + 12.0/24.0 + 6.0/15.0/24.0
angleSubtraction := 180 - MoonTimeAngle(checkTime, longitude, latitude, timeZone)
checkTime += angleSubtraction * 4.0 / 60.0 / 24.0
if HMoonHeight(checkTime, longitude, latitude, timeZone) > moonAngle {
return 0, ErrNeverSet
}
}
moonDeclination := MoonApparentDec(estimatedTime, longitude, latitude, timeZone)
tmp := (Sin(moonAngle) - Sin(moonDeclination)*Sin(latitude)) / (Cos(moonDeclination) * Cos(latitude))
if math.Abs(tmp) <= 1 && latitude < 85 {
hourAngle := (ArcCos(tmp)) / 15.0
estimatedTime += hourAngle/24 + hourAngle/33.0/15.0
} else {
i := 0
for MoonHeight(estimatedTime, longitude, latitude, timeZone) > moonAngle {
i++
estimatedTime += 15.0 / 60.0 / 24.0
if i > 48 {
break
}
}
}
// 使用牛顿迭代法求精确解
estimatedTime = moonRiseSetNewtonRaphsonIteration(estimatedTime, longitude, latitude, timeZone, moonAngle, HMoonHeight, 0.00002)
estimatedTime = estimatedTime - timeZone/24 + originalTimeZone/24
if estimatedTime > julianDayZero+1 || estimatedTime < julianDayZero {
return 0, ErrNotOnThisDate
}
return estimatedTime, nil
}
// heightFunction 高度函数类型定义,用于牛顿迭代法
type heightFunction func(time, longitude, latitude, timeZone float64) float64
// moonRiseSetNewtonRaphsonIteration 牛顿-拉夫逊迭代法求解天体高度方程
func moonRiseSetNewtonRaphsonIteration(initialTime, longitude, latitude, timeZone, targetAngle float64,
heightFunc heightFunction, tolerance float64) float64 {
const derivativeStep = 0.000005
currentTime := initialTime
for {
previousTime := currentTime
// 计算函数值:f(t) = height(t) - targetAngle
functionValue := heightFunc(previousTime, longitude, latitude, timeZone) - targetAngle
// 计算导数:f'(t) ≈ (f(t+h) - f(t-h)) / (2h)
derivative := (heightFunc(previousTime+derivativeStep, longitude, latitude, timeZone) -
heightFunc(previousTime-derivativeStep, longitude, latitude, timeZone)) / (2 * derivativeStep)
// 牛顿-拉夫逊公式:t_new = t_old - f(t) / f'(t)
currentTime = previousTime - functionValue/derivative
// 检查收敛
if math.Abs(currentTime-previousTime) <= tolerance {
break
}
}
return currentTime
}
+209
View File
@@ -0,0 +1,209 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
func MoonPhase(jd float64) float64 {
moonBo := HMoonTrueBo(jd)
sunLo := HSunApparentLo(jd)
moonLo := HMoonApparentLo(jd)
tmp := Cos(moonBo) * Cos(sunLo-moonLo)
earthSunDistance := Distance(jd) * 149597870.691
i := earthSunDistance * Sin(ArcCos(tmp)) / (HMoonAway(jd) - earthSunDistance*tmp)
i = ArcTan(i)
if i < 0 {
i += 180
}
if i > 180 {
i -= 180
}
k := (1 + Cos(i)) / 2
return k
}
func SunMoonSeek(jde float64, degree float64) float64 {
p := HMoonApparentLo(jde) - (HSunApparentLo(jde)) - degree
for p < -180 {
p += 360
}
for p > 180 {
p -= 360
}
return p
}
func CalcMoonSHByJDE(jde float64, phaseType int) float64 {
phaseType = phaseType * 180
estimateJD := jde
for {
prevJD := estimateJD
stDegree := SunMoonSeek(prevJD, float64(phaseType))
stDegreep := (SunMoonSeek(prevJD+0.000005, float64(phaseType)) - SunMoonSeek(prevJD-0.000005, float64(phaseType))) / 0.00001
estimateJD = prevJD - stDegree/stDegreep
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return estimateJD
}
func CalcMoonSH(year float64, phaseType int) float64 {
jde := CalcMoonS(year, phaseType)
phaseType = phaseType * 180
estimateJD := jde
for {
prevJD := estimateJD
stDegree := SunMoonSeek(prevJD, float64(phaseType))
stDegreep := (SunMoonSeek(prevJD+0.000005, float64(phaseType)) - SunMoonSeek(prevJD-0.000005, float64(phaseType))) / 0.00001
estimateJD = prevJD - stDegree/stDegreep
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return estimateJD
}
/*
* C=0朔月时刻 =1 望月
*/
func CalcMoonS(year float64, phaseType int) float64 {
k := math.Floor((year - 2000) * 12.36827)
if phaseType == 1 {
k += 0.5
}
T := k / 1236.85
jde := 2451550.09765 + 29.530588853*k + 0.0001337*T*T - 0.000000150*T*T*T + 0.00000000073*T*T*T*T
//太阳平近点角:
M := Limit360(2.5534 + 29.10535669*k - 0.0000218*T*T - 0.00000011*T*T*T)
//月亮的平近点角:
N := Limit360(201.5643 + 385.81693528*k + 0.0107438*T*T + 0.00001239*T*T*T - 0.000000058*T*T*T*T)
//月亮的纬度参数:
F := Limit360(160.7108 + 390.67050274*k - 0.0016341*T*T - 0.00000227*T*T*T + 0.000000011*T*T*T*T)
//月亮轨道升交点经度:
O := Limit360(124.7746 - 1.56375580*k + 0.0020691*T*T + 0.00000215*T*T*T)
E := 1 - 0.002516*T - 0.0000074*T*T
//die(E." ".M." ".N." ".F." ".O);
angles := []float64{N, M, 2 * N, 2 * F, N - M, N + M, 2 * M, N - 2*F, N + 2*F, 2*N + M, 3 * N, M + 2*F, M - 2*F, 2*N - M, O, N + 2*M, 2*N - 2*F, 3 * M, N + M - 2*F, 2*N + 2*F, N + M + 2*F, N - M + 2*F, N - M - 2*F, 3*N + M, 4 * N}
var coeffs []float64
if phaseType == 0 {
coeffs = []float64{-0.40720, 0.17241 * E, 0.01608, 0.01039, 0.00739 * E, -0.00514 * E, 0.00208 * E * E, -0.00111, -0.00057, 0.00056 * E, -0.00042, 0.00042 * E, 0.00038 * E, -0.00024 * E, -0.00017, -0.00007, 0.00004, 0.00004, 0.00003, 0.00003, -0.00003, 0.00003, -0.00002, -0.00002, 0.00002}
} else {
coeffs = []float64{-0.40614, 0.17302 * E, 0.01614, 0.01043, 0.00734 * E, -0.00515 * E, 0.00209 * E * E, -0.00111, -0.00057, 0.00056 * E, -0.00042, 0.00042 * E, 0.00038 * E, -0.00024 * E, -0.00017, -0.00007, 0.00004, 0.00004, 0.00003, 0.00003, -0.00003, 0.00003, -0.00002, -0.00002, 0.00002}
}
var correction float64
for idx, angle := range angles {
correction += Sin(angle) * coeffs[idx]
}
//die(tmp);
A1 := 299.77 + 0.107408*k - 0.009173*T*T
A2 := 251.88 + 0.016321*k
A3 := 251.83 + 26.651886*k
A4 := 349.42 + 36.412478*k
A5 := 84.66 + 18.206239*k
A6 := 141.74 + 53.303771*k
A7 := 207.14 + 2.453732*k
A8 := 154.84 + 7.306860*k
A9 := 34.52 + 27.261239*k
A10 := 207.19 + 0.121824*k
A11 := 291.34 + 1.844379*k
A12 := 161.72 + 24.198154*k
A13 := 239.56 + 25.513099*k
A14 := 331.55 + 3.592518*k
planetaryCorrection := 325*Sin(A1) + 165*Sin(A2) + 164*Sin(A3) + 126*Sin(A4) + 110*Sin(A5) + 62*Sin(A6) + 60*Sin(A7) + 56*Sin(A8) + 47*Sin(A9) + 42*Sin(A10) + 40*Sin(A11) + 37*Sin(A12) + 35*Sin(A13) + 23*Sin(A14)
planetaryCorrection /= 1000000
jde = jde + planetaryCorrection + correction
return jde
}
func CalcMoonXHByJDE(jde float64, quarterType int) float64 {
if quarterType == 0 {
quarterType = 90
} else {
quarterType = -90
}
estimateJD := jde
for {
prevJD := estimateJD
stDegree := SunMoonSeek(prevJD, float64(quarterType))
stDegreep := (SunMoonSeek(prevJD+0.000005, float64(quarterType)) - SunMoonSeek(prevJD-0.000005, float64(quarterType))) / 0.00001
estimateJD = prevJD - stDegree/stDegreep
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return estimateJD
}
func CalcMoonXH(year float64, quarterType int) float64 {
jde := CalcMoonX(year, quarterType)
if quarterType == 0 {
quarterType = 90
} else {
quarterType = -90
}
estimateJD := jde
for {
prevJD := estimateJD
stDegree := SunMoonSeek(prevJD, float64(quarterType))
stDegreep := (SunMoonSeek(prevJD+0.000005, float64(quarterType)) - SunMoonSeek(prevJD-0.000005, float64(quarterType))) / 0.00001
estimateJD = prevJD - stDegree/stDegreep
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return estimateJD
}
func CalcMoonX(year float64, quarterType int) float64 {
k := math.Floor((year-2000)*12.36827) + 0.25
if quarterType == 1 {
k += 0.5
}
T := k / 1236.85
jde := 2451550.09765 + 29.530588853*k + 0.0001337*T*T - 0.000000150*T*T*T + 0.00000000073*T*T*T*T
//太阳平近点角:
M := Limit360(2.5534 + 29.10535669*k - 0.0000218*T*T - 0.00000011*T*T*T)
//月亮的平近点角:
N := Limit360(201.5643 + 385.81693528*k + 0.0107438*T*T + 0.00001239*T*T*T - 0.000000058*T*T*T*T)
//月亮的纬度参数:
F := Limit360(160.7108 + 390.67050274*k - 0.0016341*T*T - 0.00000227*T*T*T + 0.000000011*T*T*T*T)
//月亮轨道升交点经度:
O := Limit360(124.7746 - 1.56375580*k + 0.0020691*T*T + 0.00000215*T*T*T)
E := 1 - 0.002516*T - 0.0000074*T*T
//die(E." ".M." ".N." ".F." ".O);
ZQ := []float64{N, M, N + M, 2 * N, 2 * F, N - M, 2 * M, N - 2*F, N + 2*F, 3 * N, 2*N - M, M + 2*F, M - 2*F, N + 2*M, 2*N + M, O, N - M - 2*F, 2*N + 2*F, N + M + 2*F, N - 2*F, N + M - 2*F, 3 * M, 2*N - 2*F, N - M + 2*F, M + 3*N}
MN := []float64{-0.62801, 0.17172 * E, -0.01183 * E, 0.00862, 0.00804, 0.00454 * E, 0.00204 * E * E, -0.00180, -0.00070, -0.00040, -0.00034 * E, 0.00032 * E, 0.00032 * E, -0.00028 * E * E, 0.00027 * E, -0.00017, -0.00005, 0.00004, -0.00004, 0.00004, 0.00003, 0.00003, 0.00002, 0.00002, -0.00002}
var correction float64
for idx, angle := range ZQ {
correction += Sin(angle) * MN[idx]
}
W := 0.00306 - 0.00038*E*Cos(M) + 0.00026*Cos(N) - 0.00002*Cos(N-M) + 0.00002*Cos(N+M) + 0.00002*Cos(2*F)
A1 := 299.77 + 0.107408*k - 0.009173*T*T
A2 := 251.88 + 0.016321*k
A3 := 251.83 + 26.651886*k
A4 := 349.42 + 36.412478*k
A5 := 84.66 + 18.206239*k
A6 := 141.74 + 53.303771*k
A7 := 207.14 + 2.453732*k
A8 := 154.84 + 7.306860*k
A9 := 34.52 + 27.261239*k
A10 := 207.19 + 0.121824*k
A11 := 291.34 + 1.844379*k
A12 := 161.72 + 24.198154*k
A13 := 239.56 + 25.513099*k
A14 := 331.55 + 3.592518*k
planetaryCorrection := 325*Sin(A1) + 165*Sin(A2) + 164*Sin(A3) + 126*Sin(A4) + 110*Sin(A5) + 62*Sin(A6) + 60*Sin(A7) + 56*Sin(A8) + 47*Sin(A9) + 42*Sin(A10) + 40*Sin(A11) + 37*Sin(A12) + 35*Sin(A13) + 23*Sin(A14)
planetaryCorrection /= 1000000
//die(tmp2);
//die(JDE." ".tmp." ".tmp2." ".W);
jde = jde + planetaryCorrection + correction
if quarterType == 0 {
jde += W
} else {
jde -= W
}
return jde
}
+168
View File
@@ -0,0 +1,168 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
const moonPhysicalInclinationDeg = 1.54242
const moonPhysicalAstronomicalUnitKM = 149597870.7
// MoonPhysicalInfo 月球物理观测参数 / physical observing parameters of the Moon.
type MoonPhysicalInfo struct {
// OpticalLongitude 光学经度天平动,单位度 / optical libration in longitude, degrees.
OpticalLongitude float64
// OpticalLatitude 光学纬度天平动,单位度 / optical libration in latitude, degrees.
OpticalLatitude float64
// PhysicalLongitude 物理经度天平动,单位度 / physical libration in longitude, degrees.
PhysicalLongitude float64
// PhysicalLatitude 物理纬度天平动,单位度 / physical libration in latitude, degrees.
PhysicalLatitude float64
// LibrationLongitude 总经度天平动,单位度 / total libration in longitude, degrees.
LibrationLongitude float64
// LibrationLatitude 总纬度天平动,单位度 / total libration in latitude, degrees.
LibrationLatitude float64
// PositionAngle 月球自转轴位置角,单位度 / position angle of the lunar rotation axis, degrees.
PositionAngle float64
}
// MoonPhysical 月球物理观测参数 / physical observing parameters of the Moon.
func MoonPhysical(jd float64) MoonPhysicalInfo {
return MoonPhysicalN(jd, -1)
}
// MoonPhysicalN 月球物理观测参数(截断版) / truncated physical observing parameters of the Moon.
func MoonPhysicalN(jd float64, n int) MoonPhysicalInfo {
return moonPhysicalNFromCoordinates(jd, n, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n), HMoonTrueRaN(jd, n))
}
// MoonTopocentricPhysical 月球站心物理观测参数 / topocentric physical observing parameters of the Moon.
func MoonTopocentricPhysical(jd, observerLon, observerLat, height float64) MoonPhysicalInfo {
return MoonTopocentricPhysicalN(jd, observerLon, observerLat, height, -1)
}
// MoonTopocentricPhysicalN 月球站心物理观测参数(截断版) / truncated topocentric physical observing parameters of the Moon.
func MoonTopocentricPhysicalN(jd, observerLon, observerLat, height float64, n int) MoonPhysicalInfo {
lambda, beta, alpha := moonTopocentricPhysicalCoordinatesN(jd, observerLon, observerLat, height, n)
return moonPhysicalNFromCoordinates(jd, n, lambda, beta, alpha)
}
func moonPhysicalNFromCoordinates(jd float64, n int, lambda, beta, alpha float64) MoonPhysicalInfo {
t := (jd - 2451545.0) / 36525.0
epsilon := TrueObliquity(jd)
deltaPsi := Nutation2000Bi(jd)
D := Limit360(SunMoonAngle(jd))
sunMeanAnomaly := Limit360(SunM(jd))
moonMeanAnomaly := Limit360(MoonM(jd))
F := Limit360(MoonLonX(jd))
omega := moonPhysicalMeanAscendingNode(t)
E := 1 - 0.002516*t - 0.0000074*t*t
K1 := 119.75 + 131.849*t
K2 := 72.56 + 20.186*t
W := Limit360(lambda - deltaPsi - omega)
A := ArcTan2(Sin(W)*Cos(beta)*Cos(moonPhysicalInclinationDeg)-Sin(beta)*Sin(moonPhysicalInclinationDeg), Cos(W)*Cos(beta))
opticalLongitude := wrapSignedAngle180(A - F)
opticalLatitude := ArcSin(-Sin(W)*Cos(beta)*Sin(moonPhysicalInclinationDeg) - Sin(beta)*Cos(moonPhysicalInclinationDeg))
rho, sigma, tau := moonPhysicalLibrationSeries(D, sunMeanAnomaly, moonMeanAnomaly, F, omega, E, K1, K2)
physicalLongitude := -tau + (rho*Cos(A)+sigma*Sin(A))*Tan(opticalLatitude)
physicalLatitude := sigma*Cos(A) - rho*Sin(A)
librationLongitude := wrapSignedAngle180(opticalLongitude + physicalLongitude)
librationLatitude := opticalLatitude + physicalLatitude
V := Limit360(omega + deltaPsi + sigma/Sin(moonPhysicalInclinationDeg))
X := Sin(moonPhysicalInclinationDeg+rho) * Sin(V)
Y := Sin(moonPhysicalInclinationDeg+rho)*Cos(V)*Cos(epsilon) - Cos(moonPhysicalInclinationDeg+rho)*Sin(epsilon)
littleOmega := ArcTan2(X, Y)
positionAngle := ArcSin(clampUnit((sqrtXY(X, Y) * Cos(alpha-littleOmega)) / Cos(librationLatitude)))
return MoonPhysicalInfo{
OpticalLongitude: opticalLongitude,
OpticalLatitude: opticalLatitude,
PhysicalLongitude: physicalLongitude,
PhysicalLatitude: physicalLatitude,
LibrationLongitude: librationLongitude,
LibrationLatitude: librationLatitude,
PositionAngle: positionAngle,
}
}
func moonTopocentricPhysicalCoordinatesN(jd, observerLon, observerLat, height float64, n int) (lambda, beta, alpha float64) {
geocentricRA := HMoonTrueRaN(jd, n)
geocentricDec := HMoonTrueDecN(jd, n)
distanceAU := HMoonAwayN(jd, n) / moonPhysicalAstronomicalUnitKM
utJD := TD2UT(jd, false)
var topocentricDec float64
alpha, topocentricDec = TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, utJD, distanceAU, height)
lambda, beta = RaDecToLoBo(jd, alpha, topocentricDec)
return
}
func moonPhysicalLibrationSeries(D, M, MP, F, omega, E, K1, K2 float64) (rho, sigma, tau float64) {
rho = -0.02752*Cos(MP) -
0.02245*Sin(F) +
0.00684*Cos(MP-2*F) -
0.00293*Cos(2*F) -
0.00085*Cos(2*F-2*D) -
0.00054*Cos(MP-2*D) -
0.00020*Sin(MP+F) -
0.00020*Cos(MP+2*F) -
0.00020*Cos(MP-F) +
0.00014*Cos(MP+2*F-2*D)
sigma = -0.02816*Sin(MP) +
0.02244*Cos(F) -
0.00682*Sin(MP-2*F) -
0.00279*Sin(2*F) -
0.00083*Sin(2*F-2*D) +
0.00069*Sin(MP-2*D) +
0.00040*Cos(MP+F) -
0.00025*Sin(2*MP) -
0.00023*Sin(MP+2*F) +
0.00020*Cos(MP-F) +
0.00019*Sin(MP-F) +
0.00013*Sin(MP+2*F-2*D) -
0.00010*Cos(MP-3*F)
tau = 0.02520*E*Sin(M) +
0.00473*Sin(2*MP-2*F) -
0.00467*Sin(MP) +
0.00396*Sin(K1) +
0.00276*Sin(2*MP-2*D) +
0.00196*Sin(omega) -
0.00183*Cos(MP-F) +
0.00115*Sin(MP-2*D) -
0.00096*Sin(MP-D) +
0.00046*Sin(2*F-2*D) -
0.00039*Sin(MP-F) -
0.00032*Sin(MP-M-D) +
0.00027*Sin(2*MP-M-2*D) +
0.00023*Sin(K2) -
0.00014*Sin(2*D) +
0.00014*Cos(2*MP-2*F) -
0.00012*Sin(MP-2*F) -
0.00012*Sin(2*MP) +
0.00011*Sin(2*MP-2*M-2*D)
return
}
func moonPhysicalMeanAscendingNode(t float64) float64 {
return Limit360(125.04452222222222 - 1934.136261111111*t + 0.0020708333333333334*t*t + 0.0000022222222222222222*t*t*t)
}
func wrapSignedAngle180(angle float64) float64 {
angle = Limit360(angle)
if angle > 180 {
angle -= 360
}
return angle
}
func sqrtXY(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
+68
View File
@@ -0,0 +1,68 @@
package basic
import (
"math"
"testing"
"time"
)
func TestMoonPhysicalMeeusExample(t *testing.T) {
info := MoonPhysical(2448724.5)
assertClose(t, "OpticalLongitude", info.OpticalLongitude, -1.206, 0.01)
assertClose(t, "OpticalLatitude", info.OpticalLatitude, 4.194, 0.01)
assertClose(t, "PhysicalLongitude", info.PhysicalLongitude, -0.025, 0.01)
assertClose(t, "PhysicalLatitude", info.PhysicalLatitude, 0.006, 0.01)
assertClose(t, "LibrationLongitude", info.LibrationLongitude, -1.23, 0.02)
assertClose(t, "LibrationLatitude", info.LibrationLatitude, 4.20, 0.02)
assertClose(t, "PositionAngle", info.PositionAngle, 15.08, 0.02)
}
func TestMoonPhysicalNFullMatchesDefault(t *testing.T) {
jd := 2461163.896354167
got := MoonPhysical(jd)
gotN := MoonPhysicalN(jd, -1)
assertSameFloat(t, "OpticalLongitude", got.OpticalLongitude, gotN.OpticalLongitude)
assertSameFloat(t, "OpticalLatitude", got.OpticalLatitude, gotN.OpticalLatitude)
assertSameFloat(t, "PhysicalLongitude", got.PhysicalLongitude, gotN.PhysicalLongitude)
assertSameFloat(t, "PhysicalLatitude", got.PhysicalLatitude, gotN.PhysicalLatitude)
assertSameFloat(t, "LibrationLongitude", got.LibrationLongitude, gotN.LibrationLongitude)
assertSameFloat(t, "LibrationLatitude", got.LibrationLatitude, gotN.LibrationLatitude)
assertSameFloat(t, "PositionAngle", got.PositionAngle, gotN.PositionAngle)
}
func TestMoonPhysicalSampleSweepFiniteAndInRange(t *testing.T) {
dates := []time.Time{
time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC),
time.Date(1969, 7, 20, 20, 17, 40, 0, time.UTC),
time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC),
time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC),
time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC),
}
for _, date := range dates {
jd := TD2UT(Date2JDE(date.UTC()), true)
info := MoonPhysical(jd)
prefix := date.Format(time.RFC3339)
assertFiniteSymmetric(t, prefix+".OpticalLongitude", info.OpticalLongitude, 180)
assertFiniteSymmetric(t, prefix+".OpticalLatitude", info.OpticalLatitude, 90)
assertFiniteSymmetric(t, prefix+".PhysicalLongitude", info.PhysicalLongitude, 180)
assertFiniteSymmetric(t, prefix+".PhysicalLatitude", info.PhysicalLatitude, 90)
assertFiniteSymmetric(t, prefix+".LibrationLongitude", info.LibrationLongitude, 180)
assertFiniteSymmetric(t, prefix+".LibrationLatitude", info.LibrationLatitude, 90)
assertFiniteSymmetric(t, prefix+".PositionAngle", info.PositionAngle, 90)
}
}
func assertFiniteSymmetric(t *testing.T, name string, got, limit float64) {
t.Helper()
if math.IsNaN(got) || math.IsInf(got, 0) {
t.Fatalf("%s is not finite: %.18f", name, got)
}
if got < -limit || got > limit {
t.Fatalf("%s out of range: %.18f not in [-%.18f, %.18f]", name, got, limit, limit)
}
}
File diff suppressed because one or more lines are too long
+405 -467
View File
File diff suppressed because it is too large Load Diff
+80
View File
@@ -0,0 +1,80 @@
package basic
import (
"testing"
"time"
. "b612.me/astro/tools"
)
func TestMoonTopocentricPhysicalMatchesCorrectionMethod(t *testing.T) {
jd := TD2UT(Date2JDE(testTime(2026, 4, 28, 9, 30, 45)), true)
observerLon := 121.4737
observerLat := 31.2304
got := MoonTopocentricPhysical(jd, observerLon, observerLat, 0)
want := moonTopocentricPhysicalByCorrection(jd, observerLon, observerLat)
assertPlanetPhaseClose(t, "MoonTopocentricPhysical.LibrationLongitude", got.LibrationLongitude, want.LibrationLongitude, 0.1)
assertPlanetPhaseClose(t, "MoonTopocentricPhysical.LibrationLatitude", got.LibrationLatitude, want.LibrationLatitude, 0.1)
assertPlanetPhaseClose(t, "MoonTopocentricPhysical.PositionAngle", got.PositionAngle, want.PositionAngle, 0.1)
}
func TestMoonTopocentricPhysicalSampleSweepFiniteAndInRange(t *testing.T) {
samples := []struct {
name string
jd float64
observerLon float64
observerLat float64
height float64
}{
{"shanghai", TD2UT(Date2JDE(testTime(2026, 4, 28, 9, 30, 45)), true), 121.4737, 31.2304, 4},
{"chicago", TD2UT(Date2JDE(testTime(2024, 3, 25, 7, 0, 0)), true), -87.65, 41.85, 180},
}
for _, sample := range samples {
info := MoonTopocentricPhysical(sample.jd, sample.observerLon, sample.observerLat, sample.height)
prefix := sample.name + "."
assertFiniteRange(t, prefix+"OpticalLongitude", info.OpticalLongitude, -180, 180, false)
assertFiniteRange(t, prefix+"OpticalLatitude", info.OpticalLatitude, -90, 90, false)
assertFiniteRange(t, prefix+"PhysicalLongitude", info.PhysicalLongitude, -180, 180, false)
assertFiniteRange(t, prefix+"PhysicalLatitude", info.PhysicalLatitude, -90, 90, false)
assertFiniteRange(t, prefix+"LibrationLongitude", info.LibrationLongitude, -180, 180, false)
assertFiniteRange(t, prefix+"LibrationLatitude", info.LibrationLatitude, -90, 90, false)
assertFiniteRange(t, prefix+"PositionAngle", info.PositionAngle, -90, 90, false)
}
}
func moonTopocentricPhysicalByCorrection(jd, observerLon, observerLat float64) MoonPhysicalInfo {
geocentric := MoonPhysical(jd)
moonRA := HMoonTrueRa(jd)
moonDec := HMoonTrueDec(jd)
hourAngle := StarHourAngle(TD2UT(jd, false), moonRA, observerLon, 0)
horizontalParallax := ArcSin(6378.1366 / HMoonAway(jd))
Q := ArcTan2(
Cos(moonDec)*Sin(hourAngle),
Cos(moonDec)*Sin(observerLat)-Sin(moonDec)*Cos(observerLat)*Cos(hourAngle),
)
z := ArcCos(Sin(moonDec)*Sin(observerLat) + Cos(moonDec)*Cos(observerLat)*Cos(hourAngle))
piPrime := horizontalParallax * (Sin(z) + 0.0084*Sin(2*z))
deltaL := -piPrime * Sin(Q-geocentric.PositionAngle) / Cos(geocentric.LibrationLatitude)
deltaB := piPrime * Cos(Q-geocentric.PositionAngle)
deltaP := deltaL*Sin(geocentric.LibrationLatitude+deltaB) - piPrime*Sin(Q)*Tan(moonDec)
return MoonPhysicalInfo{
OpticalLongitude: geocentric.OpticalLongitude,
OpticalLatitude: geocentric.OpticalLatitude,
PhysicalLongitude: geocentric.PhysicalLongitude,
PhysicalLatitude: geocentric.PhysicalLatitude,
LibrationLongitude: wrapSignedAngle180(geocentric.LibrationLongitude + deltaL),
LibrationLatitude: geocentric.LibrationLatitude + deltaB,
PositionAngle: geocentric.PositionAngle + deltaP,
}
}
func testTime(year int, month time.Month, day, hour, minute, second int) time.Time {
return time.Date(year, month, day, hour, minute, second, 0, time.UTC)
}
+102 -341
View File
@@ -7,143 +7,112 @@ import (
. "b612.me/astro/tools"
)
func NeptuneL(JD float64) float64 {
return planet.WherePlanet(7, 0, JD)
func NeptuneL(jd float64) float64 {
return planet.WherePlanet(7, 0, jd)
}
func NeptuneB(JD float64) float64 {
return planet.WherePlanet(7, 1, JD)
func NeptuneB(jd float64) float64 {
return planet.WherePlanet(7, 1, jd)
}
func NeptuneR(JD float64) float64 {
return planet.WherePlanet(7, 2, JD)
func NeptuneR(jd float64) float64 {
return planet.WherePlanet(7, 2, jd)
}
func ANeptuneX(JD float64) float64 {
l := NeptuneL(JD)
b := NeptuneB(JD)
r := NeptuneR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func ANeptuneX(jd float64) float64 {
l := NeptuneL(jd)
b := NeptuneB(jd)
r := NeptuneR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x
}
func ANeptuneY(JD float64) float64 {
func ANeptuneY(jd float64) float64 {
l := NeptuneL(JD)
b := NeptuneB(JD)
r := NeptuneR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
l := NeptuneL(jd)
b := NeptuneB(jd)
r := NeptuneR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y
}
func ANeptuneZ(JD float64) float64 {
//l := NeptuneL(JD)
b := NeptuneB(JD)
r := NeptuneR(JD)
// el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func ANeptuneZ(jd float64) float64 {
//l := NeptuneL(jd)
b := NeptuneB(jd)
r := NeptuneR(jd)
// el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
z := r*Sin(b) - er*Sin(eb)
return z
}
func ANeptuneXYZ(JD float64) (float64, float64, float64) {
l := NeptuneL(JD)
b := NeptuneB(JD)
r := NeptuneR(JD)
el := planet.WherePlanet(-1, 0, JD)
eb := planet.WherePlanet(-1, 1, JD)
er := planet.WherePlanet(-1, 2, JD)
func ANeptuneXYZ(jd float64) (float64, float64, float64) {
l := NeptuneL(jd)
b := NeptuneB(jd)
r := NeptuneR(jd)
el := planet.WherePlanet(-1, 0, jd)
eb := planet.WherePlanet(-1, 1, jd)
er := planet.WherePlanet(-1, 2, jd)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb)
return x, y, z
}
func NeptuneApparentRa(JD float64) float64 {
lo, bo := NeptuneApparentLoBo(JD)
sita := Sita(JD)
ra := math.Atan2((Sin(lo)*Cos(sita) - Tan(bo)*Sin(sita)), Cos(lo))
func NeptuneApparentRa(jd float64) float64 {
lo, bo := NeptuneApparentLoBo(jd)
eps := TrueObliquity(jd)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
return Limit360(ra)
}
func NeptuneApparentDec(JD float64) float64 {
lo, bo := NeptuneApparentLoBo(JD)
sita := Sita(JD)
dec := ArcSin(Sin(bo)*Cos(sita) + Cos(bo)*Sin(sita)*Sin(lo))
func NeptuneApparentDec(jd float64) float64 {
lo, bo := NeptuneApparentLoBo(jd)
eps := TrueObliquity(jd)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec
}
func NeptuneApparentRaDec(JD float64) (float64, float64) {
lo, bo := NeptuneApparentLoBo(JD)
sita := Sita(JD)
ra := math.Atan2((Sin(lo)*Cos(sita) - Tan(bo)*Sin(sita)), Cos(lo))
func NeptuneApparentRaDec(jd float64) (float64, float64) {
lo, bo := NeptuneApparentLoBo(jd)
eps := TrueObliquity(jd)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi
dec := ArcSin(Sin(bo)*Cos(sita) + Cos(bo)*Sin(sita)*Sin(lo))
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return Limit360(ra), dec
}
func EarthNeptuneAway(JD float64) float64 {
x, y, z := ANeptuneXYZ(JD)
to := math.Sqrt(x*x + y*y + z*z)
return to
func EarthNeptuneAway(jd float64) float64 {
return planetEarthAwayExplicitN(7, jd, -1)
}
func NeptuneApparentLo(JD float64) float64 {
x, y, z := ANeptuneXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ANeptuneXYZ(JD - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,JD)/3600;
//bo+=GXCBo(lo,bo,JD);
lo += Nutation2000Bi(JD)
return lo
func NeptuneApparentLo(jd float64) float64 {
geo, _ := planetApparentGeocentricPositionN(7, jd, -1)
return geo.lo
}
func NeptuneApparentBo(JD float64) float64 {
x, y, z := ANeptuneXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ANeptuneXYZ(JD - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,JD);
//bo+=GXCBo(lo,bo,JD)/3600;
//lo+=Nutation2000Bi(JD);
return bo
func NeptuneApparentBo(jd float64) float64 {
geo, _ := planetApparentGeocentricPositionN(7, jd, -1)
return geo.bo
}
func NeptuneApparentLoBo(JD float64) (float64, float64) {
x, y, z := ANeptuneXYZ(JD)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ANeptuneXYZ(JD - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,JD)/3600;
//bo+=GXCBo(lo,bo,JD);
lo += Nutation2000Bi(JD)
return lo, bo
func NeptuneApparentLoBo(jd float64) (float64, float64) {
geo, _ := planetApparentGeocentricPositionN(7, jd, -1)
return geo.lo, geo.bo
}
func NeptuneMag(JD float64) float64 {
AwaySun := NeptuneR(JD)
AwayEarth := EarthNeptuneAway(JD)
Away := planet.WherePlanet(-1, 2, JD)
i := (AwaySun*AwaySun + AwayEarth*AwayEarth - Away*Away) / (2 * AwaySun * AwayEarth)
func NeptuneMag(jd float64) float64 {
sunDistance := NeptuneR(jd)
earthDistance := EarthNeptuneAway(jd)
earthSunDistance := planet.WherePlanet(-1, 2, jd)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(i)
Mag := -6.87 + 5*math.Log10(AwaySun*AwayEarth)
return FloatRound(Mag, 2)
mag := -6.87 + 5*math.Log10(sunDistance*earthDistance)
return FloatRound(mag, 2)
}
func NeptuneHeight(jde, lon, lat, timezone float64) float64 {
@@ -153,10 +122,10 @@ func NeptuneHeight(jde, lon, lat, timezone float64) float64 {
ra, dec := NeptuneApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
// 计算时角
H := Limit360(st - ra)
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(H)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(H)
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(hourAngle)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(hourAngle)
return ArcSin(sinHeight)
}
@@ -167,271 +136,63 @@ func NeptuneAzimuth(jde, lon, lat, timezone float64) float64 {
ra, dec := NeptuneApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
// 计算时角
H := Limit360(st - ra)
hourAngle := Limit360(st - ra)
// 三角转换公式
tanAzimuth := Sin(H) / (Cos(H)*Sin(lat) - Tan(dec)*Cos(lat))
Azimuth := ArcTan(tanAzimuth)
if Azimuth < 0 {
if H/15 < 12 {
return Azimuth + 360
tanAzimuth := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(dec)*Cos(lat))
azimuth := ArcTan(tanAzimuth)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
}
return Azimuth + 180
return azimuth + 180
}
if H/15 < 12 {
return Azimuth + 180
if hourAngle/15 < 12 {
return azimuth + 180
}
return Azimuth
return azimuth
}
func NeptuneHourAngle(JD, Lon, TZ float64) float64 {
startime := Limit360(ApparentSiderealTime(JD-TZ/24)*15 + Lon)
timeangle := startime - NeptuneApparentRa(TD2UT(JD-TZ/24.0, true))
if timeangle < 0 {
timeangle += 360
func NeptuneHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon)
hourAngle := siderealLongitude - NeptuneApparentRa(TD2UT(jd-timezone/24.0, true))
if hourAngle < 0 {
hourAngle += 360
}
return timeangle
return hourAngle
}
func NeptuneCulminationTime(jde, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5
JD1 := jde + Limit360(360-NeptuneHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
limitHA := func(jde, lon, timezone float64) float64 {
ha := NeptuneHourAngle(jde, lon, timezone)
if ha < 180 {
ha += 360
estimateJD := jde + Limit360(360-NeptuneHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 {
currentHourAngle := NeptuneHourAngle(jde, lon, timezone)
if currentHourAngle < 180 {
currentHourAngle += 360
}
return ha
return currentHourAngle
}
for {
JD0 := JD1
stDegree := limitHA(JD0, lon, timezone) - 360
stDegreep := (limitHA(JD0+0.000005, lon, timezone) - limitHA(JD0-0.000005, lon, timezone)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
prevJD := estimateJD
hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360
hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001
estimateJD = prevJD - hourAngleDelta/hourAngleSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return JD1
return estimateJD
}
func NeptuneRiseTime(JD, Lon, Lat, TZ, ZS, HEI float64) float64 {
return neptuneRiseDown(JD, Lon, Lat, TZ, ZS, HEI, true)
func NeptuneRiseTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return neptuneRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true)
}
func NeptuneDownTime(JD, Lon, Lat, TZ, ZS, HEI float64) float64 {
return neptuneRiseDown(JD, Lon, Lat, TZ, ZS, HEI, false)
func NeptuneSetTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return neptuneRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false)
}
func neptuneRiseDown(JD, Lon, Lat, TZ, ZS, HEI float64, isRise bool) float64 {
var An float64
JD = math.Floor(JD) + 0.5
ntz := math.Round(Lon / 15)
if ZS != 0 {
An = -0.8333
}
An = An - HeightDegreeByLat(HEI, Lat)
tztime := NeptuneCulminationTime(JD, Lon, ntz)
if NeptuneHeight(tztime, Lon, Lat, ntz) < An {
return -2 //极夜
}
if NeptuneHeight(tztime-0.5, Lon, Lat, ntz) > An {
return -1 //极昼
}
dec := HSunApparentDec(TD2UT(tztime-ntz/24, true))
//(sin(ho)-sin(φ)*sin(δ2))/(cos(φ)*cos(δ2))
tmp := (Sin(An) - Sin(dec)*Sin(Lat)) / (Cos(dec) * Cos(Lat))
var rise float64
if math.Abs(tmp) <= 1 {
rzsc := ArcCos(tmp) / 15
if isRise {
rise = tztime - rzsc/24 - 25.0/24.0/60.0
} else {
rise = tztime + rzsc/24 - 25.0/24.0/60.0
}
} else {
rise = tztime
i := 0
//TODO:使用二分法计算
for NeptuneHeight(rise, Lon, Lat, ntz) > An {
i++
if isRise {
rise -= 15.0 / 60.0 / 24.0
} else {
rise += 15.0 / 60.0 / 24.0
}
if i > 48 {
break
}
}
}
JD1 := rise
for {
JD0 := JD1
stDegree := NeptuneHeight(JD0, Lon, Lat, ntz) - An
stDegreep := (NeptuneHeight(JD0+0.000005, Lon, Lat, ntz) - NeptuneHeight(JD0-0.000005, Lon, Lat, ntz)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
break
}
}
return JD1 - ntz/24 + TZ/24
}
// Pos
const NEPTUNE_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 4332.59))
func neptuneConjunction(jde, degree float64, next uint8) float64 {
//0=last 1=next
decSub := func(jde float64, degree float64, filter bool) float64 {
sub := Limit360(Limit360(NeptuneApparentLo(jde)-HSunApparentLo(jde)) - degree)
if filter {
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
}
return sub
}
dayCost := NEPTUNE_S_PERIOD / 360
nowSub := decSub(jde, degree, false)
if next == 0 {
jde -= (360 - nowSub) * dayCost
} else {
jde += dayCost * nowSub
}
JD1 := jde
for {
JD0 := JD1
stDegree := decSub(JD0, degree, true)
stDegreep := (decSub(JD0+0.000005, degree, true) - decSub(JD0-0.000005, degree, true)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
break
}
}
return TD2UT(JD1, false)
}
func LastNeptuneConjunction(jde float64) float64 {
return neptuneConjunction(jde, 0, 0)
}
func NextNeptuneConjunction(jde float64) float64 {
return neptuneConjunction(jde, 0, 1)
}
func LastNeptuneOpposition(jde float64) float64 {
return neptuneConjunction(jde, 180, 0)
}
func NextNeptuneOpposition(jde float64) float64 {
return neptuneConjunction(jde, 180, 1)
}
func NextNeptuneEasternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 90, 1)
}
func LastNeptuneEasternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 90, 0)
}
func NextNeptuneWesternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 270, 1)
}
func LastNeptuneWesternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 270, 0)
}
func neptuneRetrograde(jde float64, isLeft bool) float64 {
//0=last 1=next
decSub := func(jde float64, val float64) float64 {
sub := NeptuneApparentRa(jde+val) - NeptuneApparentRa(jde-val)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * val)
}
jde = NextNeptuneOpposition(jde)
if isLeft {
jde -= 60
} else {
jde += 60
}
for {
nowSub := decSub(jde, 1.0/86400.0)
if math.Abs(nowSub) > 0.55 {
jde += 2
continue
}
break
}
JD1 := jde
for {
JD0 := JD1
stDegree := decSub(JD0, 2.0/86400.0)
stDegreep := (decSub(JD0+15.0/86400.0, 2.0/86400.0) - decSub(JD0-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 30.0/86400.0 {
break
}
}
JD1 = JD1 - 15.0/86400.0
min := JD1
minRa := 100.0
for i := 0.0; i < 60.0; i++ {
tmp := decSub(JD1+i*0.5/86400.0, 0.5/86400.0)
if math.Abs(tmp) < math.Abs(minRa) {
minRa = tmp
min = JD1 + i*0.5/86400.0
}
}
return TD2UT(min, false)
}
func NextNeptuneRetrogradeToPrograde(jde float64) float64 {
date := neptuneRetrograde(jde, false)
if date < jde {
op := NextNeptuneOpposition(jde)
return neptuneRetrograde(op+10, false)
}
return date
}
func LastNeptuneRetrogradeToPrograde(jde float64) float64 {
jde = LastNeptuneOpposition(jde) - 10
date := neptuneRetrograde(jde, false)
if date > jde {
op := LastNeptuneOpposition(jde)
return neptuneRetrograde(op-10, false)
}
return date
}
func NextNeptuneProgradeToRetrograde(jde float64) float64 {
date := neptuneRetrograde(jde, true)
if date < jde {
op := NextNeptuneOpposition(jde)
return neptuneRetrograde(op+10, true)
}
return date
}
func LastNeptuneProgradeToRetrograde(jde float64) float64 {
jde = LastNeptuneOpposition(jde) - 10
date := neptuneRetrograde(jde, true)
if date > jde {
op := LastNeptuneOpposition(jde)
return neptuneRetrograde(op-10, true)
}
return date
func neptuneRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool) (float64, error) {
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, NeptuneCulminationTime, NeptuneHeight, NeptuneApparentDec)
}
+207
View File
@@ -0,0 +1,207 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
// Pos
const (
NEPTUNE_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 60190.03))
neptuneEventSearchN = 16
neptunePhaseCoarseTolerance = 30.0 / 86400.0
)
func neptuneSunLongitudeDelta(jde, degree float64, filter bool) float64 {
sub := Limit360(Limit360(NeptuneApparentLo(jde)-HSunApparentLo(jde)) - degree)
if filter {
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
}
return sub
}
func neptuneSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64 {
sub := Limit360(Limit360(NeptuneApparentLoN(jde, n)-HSunApparentLoN(jde, n)) - degree)
if filter {
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
}
return sub
}
func neptuneRADerivative(jde, delta float64) float64 {
sub := NeptuneApparentRa(jde+delta) - NeptuneApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func neptuneRADerivativeN(jde, delta float64, n int) float64 {
sub := NeptuneApparentRaN(jde+delta, n) - NeptuneApparentRaN(jde-delta, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func neptuneConjunctionFull(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := NEPTUNE_S_PERIOD / 360
currentDelta := neptuneSunLongitudeDelta(jde, degree, false)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
for {
prevJD := estimateJD
longitudeDelta := neptuneSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (neptuneSunLongitudeDelta(prevJD+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
estimateJD = prevJD - longitudeDelta/longitudeSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return TD2UT(estimateJD, false)
}
func neptuneConjunction(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := NEPTUNE_S_PERIOD / 360
currentDelta := neptuneSunLongitudeDelta(jde, degree, false)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
jde += daysPerDegree * currentDelta
}
estimateJD := jde
for {
prevJD := estimateJD
longitudeDelta := neptuneSunLongitudeDeltaN(prevJD, degree, true, neptuneEventSearchN)
longitudeSlope := (neptuneSunLongitudeDeltaN(prevJD+0.000005, degree, true, neptuneEventSearchN) - neptuneSunLongitudeDeltaN(prevJD-0.000005, degree, true, neptuneEventSearchN)) / 0.00001
estimateJD = prevJD - longitudeDelta/longitudeSlope
if math.Abs(estimateJD-prevJD) <= neptunePhaseCoarseTolerance {
break
}
}
for {
prevJD := estimateJD
longitudeDelta := neptuneSunLongitudeDelta(prevJD, degree, true)
longitudeSlope := (neptuneSunLongitudeDelta(prevJD+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001
estimateJD = prevJD - longitudeDelta/longitudeSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return TD2UT(estimateJD, false)
}
func LastNeptuneConjunction(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 0, neptuneConjunction)
}
func NextNeptuneConjunction(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 0, neptuneConjunction)
}
func LastNeptuneOpposition(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 180, neptuneConjunction)
}
func NextNeptuneOpposition(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 180, neptuneConjunction)
}
func NextNeptuneEasternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 90, neptuneConjunction)
}
func LastNeptuneEasternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 90, neptuneConjunction)
}
func NextNeptuneWesternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 270, neptuneConjunction)
}
func LastNeptuneWesternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 270, neptuneConjunction)
}
func neptuneRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
oppositionTT := TD2UT(oppositionJD, true)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
easternQuadratureUT := neptuneConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
} else {
westernQuadratureUT := neptuneConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true)
}
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return neptuneRADerivativeN(jd, 1.0/86400.0, neptuneEventSearchN)
}, func(jd float64) float64 {
return neptuneRADerivative(jd, 0.5/86400.0)
})
return TD2UT(bestJD, false)
}
func NextNeptuneRetrogradeToPrograde(jde float64) float64 {
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
date := neptuneRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
return neptuneRetrogradeAroundOpposition(nextOppositionJD, false)
}
func LastNeptuneRetrogradeToPrograde(jde float64) float64 {
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
date := neptuneRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
previousOppositionJD := neptuneConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return neptuneRetrogradeAroundOpposition(previousOppositionJD, false)
}
func NextNeptuneProgradeToRetrograde(jde float64) float64 {
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
date := neptuneRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
followingOppositionJD := neptuneConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return neptuneRetrogradeAroundOpposition(followingOppositionJD, true)
}
func LastNeptuneProgradeToRetrograde(jde float64) float64 {
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
date := neptuneRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
return neptuneRetrogradeAroundOpposition(lastOppositionJD, true)
}
+1 -1
View File
@@ -15,7 +15,7 @@ func EclipticObliquity(jde float64, nutation bool) float64 {
return eps
}
func Sita(JD float64) float64 {
func TrueObliquity(JD float64) float64 {
return EclipticObliquity(JD, true)
}
+233
View File
@@ -0,0 +1,233 @@
package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
var orbitJ2000Obliquity = EclipticObliquity(orbitReferenceJD, false)
// OrbitHeliocentricXYZJ2000 返回日心 J2000 平黄道直角坐标,单位 AU。
func OrbitHeliocentricXYZJ2000(jd float64, elements OrbitElements) Vector3 {
trueAnomaly, radius, resolved, ok := orbitTrueAnomalyAndRadius(jd, elements)
if !ok {
nan := math.NaN()
return Vector3{nan, nan, nan}
}
ascendingNode := resolved.Omega * rad
argumentLatitude := resolved.W*rad + trueAnomaly
inclination := resolved.I * rad
sinAscendingNode, cosAscendingNode := math.Sincos(ascendingNode)
sinArgumentLatitude, cosArgumentLatitude := math.Sincos(argumentLatitude)
sinInclination, cosInclination := math.Sincos(inclination)
return Vector3{
radius * (cosAscendingNode*cosArgumentLatitude - sinAscendingNode*sinArgumentLatitude*cosInclination),
radius * (sinAscendingNode*cosArgumentLatitude + cosAscendingNode*sinArgumentLatitude*cosInclination),
radius * sinArgumentLatitude * sinInclination,
}
}
// OrbitHeliocentricEclipticJ2000 返回日心 J2000 平黄道球坐标,单位度/AU。
func OrbitHeliocentricEclipticJ2000(jd float64, elements OrbitElements) (lon, lat, distance float64) {
return orbitVectorToEcliptic(OrbitHeliocentricXYZJ2000(jd, elements))
}
// OrbitHeliocentricXYZ 返回日心历元黄道直角坐标,单位 AU。
func OrbitHeliocentricXYZ(jd float64, elements OrbitElements) Vector3 {
return eclipticVectorAtReferenceEpoch(OrbitHeliocentricXYZJ2000(jd, elements), orbitReferenceJD, jd)
}
// OrbitHeliocentricEcliptic 返回日心历元黄道球坐标,单位度/AU。
func OrbitHeliocentricEcliptic(jd float64, elements OrbitElements) (lon, lat, distance float64) {
return orbitVectorToEcliptic(OrbitHeliocentricXYZ(jd, elements))
}
// OrbitGeocentricXYZJ2000 返回地心 J2000 平黄道直角坐标,单位 AU。
func OrbitGeocentricXYZJ2000(jd float64, elements OrbitElements) Vector3 {
objectVector := OrbitHeliocentricXYZJ2000(jd, elements)
earthVector := earthHeliocentricVectorJ2000(jd)
return Vector3{
objectVector[0] - earthVector[0],
objectVector[1] - earthVector[1],
objectVector[2] - earthVector[2],
}
}
// OrbitGeocentricEclipticJ2000 返回地心 J2000 平黄道球坐标,单位度/AU。
func OrbitGeocentricEclipticJ2000(jd float64, elements OrbitElements) (lon, lat, distance float64) {
return orbitVectorToEcliptic(OrbitGeocentricXYZJ2000(jd, elements))
}
// OrbitGeocentricXYZ 返回地心历元黄道直角坐标,单位 AU。
func OrbitGeocentricXYZ(jd float64, elements OrbitElements) Vector3 {
objectVector := OrbitHeliocentricXYZ(jd, elements)
earthVector := earthHeliocentricVectorOfDate(jd)
return Vector3{
objectVector[0] - earthVector[0],
objectVector[1] - earthVector[1],
objectVector[2] - earthVector[2],
}
}
// OrbitGeocentricEcliptic 返回地心历元黄道球坐标,单位度/AU。
func OrbitGeocentricEcliptic(jd float64, elements OrbitElements) (lon, lat, distance float64) {
return orbitVectorToEcliptic(OrbitGeocentricXYZ(jd, elements))
}
// OrbitGeocentricEquatorialJ2000 返回地心 J2000 平赤道球坐标,单位度/AU。
func OrbitGeocentricEquatorialJ2000(jd float64, elements OrbitElements) (ra, dec, distance float64) {
vector := rotateEclipticToEquatorial(OrbitGeocentricXYZJ2000(jd, elements), orbitJ2000Obliquity)
return orbitVectorToEquatorial(vector)
}
// OrbitGeocentricEquatorial 返回地心历元平赤道球坐标,单位度/AU。
func OrbitGeocentricEquatorial(jd float64, elements OrbitElements) (ra, dec, distance float64) {
vector := rotateEclipticToEquatorial(OrbitGeocentricXYZ(jd, elements), EclipticObliquity(jd, false))
return orbitVectorToEquatorial(vector)
}
// OrbitAstrometricGeocentricXYZJ2000 返回光行时修正后的地心 J2000 平黄道直角坐标,单位 AU。
func OrbitAstrometricGeocentricXYZJ2000(jd float64, elements OrbitElements) Vector3 {
if !isFinite(jd) {
nan := math.NaN()
return Vector3{nan, nan, nan}
}
earthVector := earthHeliocentricVectorJ2000(jd)
lightTime := 0.0
result := Vector3{}
for i := 0; i < 8; i++ {
objectVector := OrbitHeliocentricXYZJ2000(jd-lightTime, elements)
result = Vector3{
objectVector[0] - earthVector[0],
objectVector[1] - earthVector[1],
objectVector[2] - earthVector[2],
}
nextLightTime := lightTimeDaysPerAU * orbitVectorNorm(result)
if math.Abs(nextLightTime-lightTime) < 1e-12 {
break
}
lightTime = nextLightTime
}
return result
}
// OrbitAstrometricGeocentricEquatorialJ2000 返回光行时修正后的地心 J2000 赤道坐标,单位度/AU。
func OrbitAstrometricGeocentricEquatorialJ2000(jd float64, elements OrbitElements) (ra, dec, distance float64) {
vector := rotateEclipticToEquatorial(OrbitAstrometricGeocentricXYZJ2000(jd, elements), orbitJ2000Obliquity)
return orbitVectorToEquatorial(vector)
}
// OrbitApparentGeocentricEcliptic 返回光行时与章动修正后的地心视黄道坐标,单位度/AU。
func OrbitApparentGeocentricEcliptic(jd float64, elements OrbitElements) (lon, lat, distance float64) {
vectorDate := eclipticVectorAtReferenceEpoch(OrbitAstrometricGeocentricXYZJ2000(jd, elements), orbitReferenceJD, jd)
lon, lat, distance = orbitVectorToEcliptic(vectorDate)
if math.IsNaN(lon) {
return math.NaN(), math.NaN(), math.NaN()
}
lon = Limit360(lon + Nutation2000Bi(jd))
return lon, lat, distance
}
// OrbitApparentGeocentricEquatorial 返回光行时与章动修正后的地心视赤道坐标,单位度/AU。
func OrbitApparentGeocentricEquatorial(jd float64, elements OrbitElements) (ra, dec, distance float64) {
lon, lat, distance := OrbitApparentGeocentricEcliptic(jd, elements)
if math.IsNaN(lon) {
return math.NaN(), math.NaN(), math.NaN()
}
ra, dec = LoBoToRaDec(jd, lon, lat)
return ra, dec, distance
}
// OrbitApparentTopocentricEquatorial 返回光行时、章动与站心修正后的视赤道坐标,单位度/AU。
func OrbitApparentTopocentricEquatorial(jd, observerLon, observerLat, observerHeight float64, elements OrbitElements) (ra, dec, distance float64) {
geocentricRA, geocentricDec, geocentricDistance := OrbitApparentGeocentricEquatorial(jd, elements)
if math.IsNaN(geocentricRA) {
return math.NaN(), math.NaN(), math.NaN()
}
geocentricVector := orbitEquatorialVector(geocentricRA, geocentricDec, geocentricDistance)
observerVector := orbitObserverEquatorialVectorOfDate(TD2UT(jd, false), observerLon, observerLat, observerHeight)
topocentricVector := Vector3{
geocentricVector[0] - observerVector[0],
geocentricVector[1] - observerVector[1],
geocentricVector[2] - observerVector[2],
}
return orbitVectorToEquatorial(topocentricVector)
}
func earthHeliocentricVectorOfDate(jd float64) Vector3 {
return eclipticCartesian(
planet.WherePlanet(-1, 0, jd),
planet.WherePlanet(-1, 1, jd),
planet.WherePlanet(-1, 2, jd),
)
}
func earthHeliocentricVectorJ2000(jd float64) Vector3 {
return eclipticVectorAtReferenceEpoch(earthHeliocentricVectorOfDate(jd), jd, orbitReferenceJD)
}
func orbitVectorToEcliptic(vector Vector3) (lon, lat, distance float64) {
distance = orbitVectorNorm(vector)
if math.IsNaN(distance) || math.IsInf(distance, 0) {
return math.NaN(), math.NaN(), math.NaN()
}
if distance == 0 {
return 0, 0, 0
}
lon = Limit360(math.Atan2(vector[1], vector[0]) * deg)
lat = math.Asin(orbitClampUnit(vector[2]/distance)) * deg
return lon, lat, distance
}
func orbitVectorToEquatorial(vector Vector3) (ra, dec, distance float64) {
distance = orbitVectorNorm(vector)
if math.IsNaN(distance) || math.IsInf(distance, 0) {
return math.NaN(), math.NaN(), math.NaN()
}
if distance == 0 {
return 0, 0, 0
}
ra = Limit360(math.Atan2(vector[1], vector[0]) * deg)
dec = math.Asin(orbitClampUnit(vector[2]/distance)) * deg
return ra, dec, distance
}
func orbitEquatorialVector(ra, dec, distance float64) Vector3 {
cosDec := Cos(dec)
return Vector3{
distance * cosDec * Cos(ra),
distance * cosDec * Sin(ra),
distance * Sin(dec),
}
}
func orbitObserverEquatorialVectorOfDate(jdUT, observerLon, observerLat, observerHeight float64) Vector3 {
localApparentSiderealLongitude := Limit360(ApparentSiderealTime(jdUT)*15 + observerLon)
observerScaleAU := Sin(0.0024427777777)
rhoCosPhiPrime := pcosi(observerLat, observerHeight)
rhoSinPhiPrime := psini(observerLat, observerHeight)
return Vector3{
observerScaleAU * rhoCosPhiPrime * Cos(localApparentSiderealLongitude),
observerScaleAU * rhoCosPhiPrime * Sin(localApparentSiderealLongitude),
observerScaleAU * rhoSinPhiPrime,
}
}
func orbitVectorNorm(vector Vector3) float64 {
return math.Sqrt(vector[0]*vector[0] + vector[1]*vector[1] + vector[2]*vector[2])
}
func orbitClampUnit(value float64) float64 {
if value > 1 {
return 1
}
if value < -1 {
return -1
}
return value
}
+96
View File
@@ -0,0 +1,96 @@
package basic
import "math"
const (
orbitReferenceJD = 2451545.0
gaussianGravitationalConstant = 0.01720209895 // rad/day
lightTimeDaysPerAU = 0.0057755183
orbitParabolicTolerance = 1e-12
)
// OrbitElements 日心二体圆锥曲线根数,参考系为 J2000 平黄道/平春分点。
// EpochJD 与 TpJD 使用 TT/TDB 对应的儒略日。
//
// 两种输入形式:
// 1. 椭圆经典根数:A/E/I/Omega/W/M0(原有形式)
// 2. 近日点形式:Q/E/I/Omega/W/TpJD,用于高偏心、抛物或双曲轨道
//
// 线性 rates 仅作用于经典根数形式,单位均为“每天变化量”。
type OrbitElements struct {
EpochJD float64
A float64 // 半长径 / semi-major axis in AU.
E float64 // 离心率 / eccentricity.
I float64 // 轨道倾角 / inclination in degrees.
Omega float64 // 升交点黄经 / longitude of ascending node in degrees.
W float64 // 近日点幅角 / argument of perihelion in degrees.
M0 float64 // 历元平近点角 / mean anomaly at epoch in degrees.
Q float64 // 近日点距离 / perihelion distance in AU.
TpJD float64 // 近日点通过时刻 / perihelion passage TT/TDB Julian day.
ADot float64 // 半长径日变化 / daily rate of A in AU/day.
EDot float64 // 离心率日变化 / daily rate of E per day.
IDot float64 // 倾角日变化 / daily rate of I in deg/day.
OmegaDot float64 // 升交点黄经日变化 / daily rate of Omega in deg/day.
WDot float64 // 近日点幅角日变化 / daily rate of W in deg/day.
MDot float64 // 平近点角日变化 / daily rate of M in deg/day.
}
func (elements OrbitElements) usesPerihelionForm() bool {
return isFinitePositive(elements.Q) && isFinite(elements.TpJD)
}
func (elements OrbitElements) validOrientation() bool {
angles := [...]float64{elements.I, elements.Omega, elements.W}
for _, angle := range angles {
if !isFinite(angle) {
return false
}
}
return true
}
func (elements OrbitElements) validEllipticClassical() bool {
if !isFinite(elements.EpochJD) || !isFinitePositive(elements.A) {
return false
}
if !isFinite(elements.E) || elements.E < 0 || elements.E >= 1 {
return false
}
if !isFinite(elements.M0) {
return false
}
return elements.validOrientation()
}
func (elements OrbitElements) validPerihelionForm() bool {
if !elements.usesPerihelionForm() {
return false
}
if !isFinite(elements.E) || elements.E < 0 {
return false
}
return elements.validOrientation()
}
func orbitElementsAt(jd float64, elements OrbitElements) OrbitElements {
if elements.usesPerihelionForm() || !isFinite(jd) || !isFinite(elements.EpochJD) {
return elements
}
deltaDays := jd - elements.EpochJD
updated := elements
updated.A += updated.ADot * deltaDays
updated.E += updated.EDot * deltaDays
updated.I += updated.IDot * deltaDays
updated.Omega += updated.OmegaDot * deltaDays
updated.W += updated.WDot * deltaDays
return updated
}
func isFinite(value float64) bool {
return !(math.IsNaN(value) || math.IsInf(value, 0))
}
func isFinitePositive(value float64) bool {
return isFinite(value) && value > 0
}
+238
View File
@@ -0,0 +1,238 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
// OrbitMeanMotion 返回历元平均角速度,单位度/日。
// 对近日点形式的抛物/双曲轨道返回 NaN。
func OrbitMeanMotion(elements OrbitElements) float64 {
if elements.usesPerihelionForm() {
if !elements.validPerihelionForm() || elements.E >= 1 {
return math.NaN()
}
semiMajorAxis := elements.Q / (1 - elements.E)
if !isFinitePositive(semiMajorAxis) {
return math.NaN()
}
if isFinite(elements.MDot) && elements.MDot != 0 {
return elements.MDot
}
return gaussianGravitationalConstant / math.Pow(semiMajorAxis, 1.5) * deg
}
if !elements.validEllipticClassical() {
return math.NaN()
}
if isFinite(elements.MDot) && elements.MDot != 0 {
return elements.MDot
}
return gaussianGravitationalConstant / math.Pow(elements.A, 1.5) * deg
}
// OrbitMeanAnomaly 返回给定 TT/TDB 儒略日的平近点角,单位度。
// 对抛物/双曲轨道返回 NaN。
func OrbitMeanAnomaly(jd float64, elements OrbitElements) float64 {
meanAnomalyDeg, ok := orbitMeanAnomalyDegAt(jd, elements)
if !ok {
return math.NaN()
}
return Limit360(meanAnomalyDeg)
}
// OrbitEccentricAnomaly 返回给定 TT/TDB 儒略日的偏近点角,单位度。
// 仅适用于椭圆轨道;对抛物/双曲轨道返回 NaN。
func OrbitEccentricAnomaly(jd float64, elements OrbitElements) float64 {
resolved := orbitElementsAt(jd, elements)
meanAnomalyDeg, ok := orbitMeanAnomalyDegAt(jd, elements)
if !ok || !resolved.validEllipticClassical() && !(resolved.validPerihelionForm() && resolved.E < 1) {
return math.NaN()
}
eccentricAnomaly, ok := orbitEccentricAnomalyRad(meanAnomalyDeg*rad, resolved.E)
if !ok {
return math.NaN()
}
return Limit360(eccentricAnomaly * deg)
}
// OrbitTrueAnomaly 返回给定 TT/TDB 儒略日的真近点角,单位度。
func OrbitTrueAnomaly(jd float64, elements OrbitElements) float64 {
trueAnomaly, _, _, ok := orbitTrueAnomalyAndRadius(jd, elements)
if !ok {
return math.NaN()
}
return Limit360(trueAnomaly * deg)
}
func orbitMeanAnomalyDegAt(jd float64, elements OrbitElements) (float64, bool) {
if !isFinite(jd) {
return math.NaN(), false
}
if elements.usesPerihelionForm() {
if !elements.validPerihelionForm() || elements.E >= 1 {
return math.NaN(), false
}
semiMajorAxis := elements.Q / (1 - elements.E)
if !isFinitePositive(semiMajorAxis) {
return math.NaN(), false
}
meanMotion := elements.MDot
if !isFinite(meanMotion) || meanMotion == 0 {
meanMotion = gaussianGravitationalConstant / math.Pow(semiMajorAxis, 1.5) * deg
}
return meanMotion * (jd - elements.TpJD), true
}
resolved := orbitElementsAt(jd, elements)
if !resolved.validEllipticClassical() {
return math.NaN(), false
}
if isFinite(elements.MDot) && elements.MDot != 0 {
return elements.M0 + elements.MDot*(jd-elements.EpochJD), true
}
meanMotion := gaussianGravitationalConstant / math.Pow(resolved.A, 1.5) * deg
return resolved.M0 + meanMotion*(jd-elements.EpochJD), true
}
func orbitEccentricAnomalyRad(meanAnomalyRad, eccentricity float64) (float64, bool) {
if !isFinite(meanAnomalyRad) || !isFinite(eccentricity) || eccentricity < 0 || eccentricity >= 1 {
return math.NaN(), false
}
if meanAnomalyRad > math.Pi {
meanAnomalyRad -= 2 * math.Pi
} else if meanAnomalyRad < -math.Pi {
meanAnomalyRad += 2 * math.Pi
}
eccentricAnomaly := meanAnomalyRad
if eccentricity >= 0.8 {
eccentricAnomaly = math.Pi
if meanAnomalyRad < 0 {
eccentricAnomaly = -math.Pi
}
}
for i := 0; i < 32; i++ {
sinE, cosE := math.Sincos(eccentricAnomaly)
delta := (eccentricAnomaly - eccentricity*sinE - meanAnomalyRad) / (1 - eccentricity*cosE)
eccentricAnomaly -= delta
if math.Abs(delta) < 1e-14 {
return eccentricAnomaly, true
}
}
return eccentricAnomaly, true
}
func orbitHyperbolicAnomaly(meanAnomaly, eccentricity float64) (float64, bool) {
if !isFinite(meanAnomaly) || !isFinite(eccentricity) || eccentricity <= 1 {
return math.NaN(), false
}
hyperbolicAnomaly := math.Asinh(meanAnomaly / eccentricity)
if hyperbolicAnomaly == 0 && meanAnomaly != 0 {
hyperbolicAnomaly = math.Log(2*math.Abs(meanAnomaly)/eccentricity + 1.8)
if meanAnomaly < 0 {
hyperbolicAnomaly = -hyperbolicAnomaly
}
}
for i := 0; i < 32; i++ {
sinhH := math.Sinh(hyperbolicAnomaly)
coshH := math.Cosh(hyperbolicAnomaly)
delta := (eccentricity*sinhH - hyperbolicAnomaly - meanAnomaly) / (eccentricity*coshH - 1)
hyperbolicAnomaly -= delta
if math.Abs(delta) < 1e-14 {
return hyperbolicAnomaly, true
}
}
return hyperbolicAnomaly, true
}
func orbitTrueAnomalyAndRadius(jd float64, elements OrbitElements) (trueAnomaly, radius float64, resolved OrbitElements, ok bool) {
resolved = orbitElementsAt(jd, elements)
if resolved.usesPerihelionForm() {
if !resolved.validPerihelionForm() {
return math.NaN(), math.NaN(), resolved, false
}
switch {
case math.Abs(resolved.E-1) <= orbitParabolicTolerance:
return orbitParabolicTrueAnomalyAndRadius(jd, resolved)
case resolved.E < 1:
return orbitEllipticTrueAnomalyAndRadiusFromPerihelion(jd, resolved)
default:
return orbitHyperbolicTrueAnomalyAndRadius(jd, resolved)
}
}
if !resolved.validEllipticClassical() {
return math.NaN(), math.NaN(), resolved, false
}
meanAnomalyDeg, ok := orbitMeanAnomalyDegAt(jd, elements)
if !ok {
return math.NaN(), math.NaN(), resolved, false
}
trueAnomaly, radius, ok = orbitEllipticTrueAnomalyAndRadius(meanAnomalyDeg*rad, resolved.A, resolved.E)
return trueAnomaly, radius, resolved, ok
}
func orbitEllipticTrueAnomalyAndRadiusFromPerihelion(jd float64, elements OrbitElements) (trueAnomaly, radius float64, resolved OrbitElements, ok bool) {
semiMajorAxis := elements.Q / (1 - elements.E)
if !isFinitePositive(semiMajorAxis) {
return math.NaN(), math.NaN(), elements, false
}
meanAnomalyDeg, ok := orbitMeanAnomalyDegAt(jd, elements)
if !ok {
return math.NaN(), math.NaN(), elements, false
}
trueAnomaly, radius, ok = orbitEllipticTrueAnomalyAndRadius(meanAnomalyDeg*rad, semiMajorAxis, elements.E)
if !ok {
return math.NaN(), math.NaN(), elements, false
}
resolved = elements
resolved.A = semiMajorAxis
return trueAnomaly, radius, resolved, true
}
func orbitEllipticTrueAnomalyAndRadius(meanAnomalyRad, semiMajorAxis, eccentricity float64) (float64, float64, bool) {
eccentricAnomaly, ok := orbitEccentricAnomalyRad(meanAnomalyRad, eccentricity)
if !ok {
return math.NaN(), math.NaN(), false
}
sinE, cosE := math.Sincos(eccentricAnomaly)
radius := semiMajorAxis * (1 - eccentricity*cosE)
trueAnomaly := math.Atan2(math.Sqrt(1-eccentricity*eccentricity)*sinE, cosE-eccentricity)
return trueAnomaly, radius, true
}
func orbitParabolicTrueAnomalyAndRadius(jd float64, elements OrbitElements) (trueAnomaly, radius float64, resolved OrbitElements, ok bool) {
if !isFinitePositive(elements.Q) || !isFinite(elements.TpJD) {
return math.NaN(), math.NaN(), elements, false
}
w := 1.5 * gaussianGravitationalConstant * (jd - elements.TpJD) / (math.Sqrt2 * math.Pow(elements.Q, 1.5))
y := math.Cbrt(w + math.Sqrt(w*w+1))
if y == 0 {
return 0, elements.Q, elements, true
}
d := y - 1/y
trueAnomaly = 2 * math.Atan(d)
radius = elements.Q * (1 + d*d)
resolved = elements
return trueAnomaly, radius, resolved, true
}
func orbitHyperbolicTrueAnomalyAndRadius(jd float64, elements OrbitElements) (trueAnomaly, radius float64, resolved OrbitElements, ok bool) {
if !isFinitePositive(elements.Q) || !isFinite(elements.TpJD) || !isFinite(elements.E) || elements.E <= 1 {
return math.NaN(), math.NaN(), elements, false
}
semiMajorAxis := elements.Q / (elements.E - 1)
meanAnomaly := gaussianGravitationalConstant * (jd - elements.TpJD) / math.Pow(semiMajorAxis, 1.5)
hyperbolicAnomaly, ok := orbitHyperbolicAnomaly(meanAnomaly, elements.E)
if !ok {
return math.NaN(), math.NaN(), elements, false
}
radius = semiMajorAxis * (elements.E*math.Cosh(hyperbolicAnomaly) - 1)
trueAnomaly = 2 * math.Atan(math.Sqrt((elements.E+1)/(elements.E-1))*math.Tanh(hyperbolicAnomaly/2))
resolved = elements
resolved.A = -semiMajorAxis
return trueAnomaly, radius, resolved, true
}
+45
View File
@@ -0,0 +1,45 @@
package basic
import "math"
// OrbitAsteroidMagnitudeHG 返回小行星 H-G 模型的视星等。
func OrbitAsteroidMagnitudeHG(jd float64, elements OrbitElements, absoluteMagnitude, slopeParameter float64) float64 {
if !isFinite(jd) || !isFinite(absoluteMagnitude) || !isFinite(slopeParameter) {
return math.NaN()
}
sunDistance := OrbitSunDistance(jd, elements)
earthDistance := OrbitEarthDistance(jd, elements)
phaseAngle := OrbitPhaseAngle(jd, elements)
if !isFinitePositive(sunDistance) || !isFinitePositive(earthDistance) || !isFinite(phaseAngle) {
return math.NaN()
}
phaseBlend := orbitHGSlopeBlend(phaseAngle, slopeParameter)
if phaseBlend == 0 {
return math.Inf(1)
}
if !isFinitePositive(phaseBlend) {
return math.NaN()
}
return absoluteMagnitude + 5*math.Log10(sunDistance*earthDistance) - 2.5*math.Log10(phaseBlend)
}
func orbitHGSlopeBlend(phaseAngle, slopeParameter float64) float64 {
phi1 := orbitHGPhaseFunction1(phaseAngle)
phi2 := orbitHGPhaseFunction2(phaseAngle)
return (1-slopeParameter)*phi1 + slopeParameter*phi2
}
func orbitHGPhaseFunction1(phaseAngle float64) float64 {
return math.Exp(-3.33 * math.Pow(orbitHGTanHalfPhaseAngle(phaseAngle), 0.63))
}
func orbitHGPhaseFunction2(phaseAngle float64) float64 {
return math.Exp(-1.87 * math.Pow(orbitHGTanHalfPhaseAngle(phaseAngle), 1.22))
}
func orbitHGTanHalfPhaseAngle(phaseAngle float64) float64 {
return math.Tan((phaseAngle * math.Pi / 180) / 2)
}
+136
View File
@@ -0,0 +1,136 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
func orbitTopocentricObservation(jde, observerLon, observerLat, observerHeight, timezone float64, elements OrbitElements) (ra, dec, distance float64) {
utcJde := jde - timezone/24.0
return OrbitApparentTopocentricEquatorial(TD2UT(utcJde, true), observerLon, observerLat, observerHeight, elements)
}
// OrbitHeight 返回轨道目标在观测者所在地的视高度角,单位度。
func OrbitHeight(jde, observerLon, observerLat, timezone, observerHeight float64, elements OrbitElements) float64 {
ra, dec, _ := orbitTopocentricObservation(jde, observerLon, observerLat, observerHeight, timezone, elements)
st := Limit360(ApparentSiderealTime(jde-timezone/24.0)*15 + observerLon)
hourAngle := Limit360(st - ra)
sinHeight := Sin(observerLat)*Sin(dec) + Cos(dec)*Cos(observerLat)*Cos(hourAngle)
return ArcSin(sinHeight)
}
// OrbitAzimuth 返回轨道目标在观测者所在地的视方位角,按正北为 0°、向东增加。
func OrbitAzimuth(jde, observerLon, observerLat, timezone, observerHeight float64, elements OrbitElements) float64 {
ra, dec, _ := orbitTopocentricObservation(jde, observerLon, observerLat, observerHeight, timezone, elements)
st := Limit360(ApparentSiderealTime(jde-timezone/24.0)*15 + observerLon)
hourAngle := Limit360(st - ra)
tanAzimuth := Sin(hourAngle) / (Cos(hourAngle)*Sin(observerLat) - Tan(dec)*Cos(observerLat))
azimuth := ArcTan(tanAzimuth)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
}
return azimuth + 180
}
if hourAngle/15 < 12 {
return azimuth + 180
}
return azimuth
}
// OrbitHourAngle 返回轨道目标的站心视时角,单位度。
func OrbitHourAngle(jde, observerLon, observerLat, timezone, observerHeight float64, elements OrbitElements) float64 {
ra, _, _ := orbitTopocentricObservation(jde, observerLon, observerLat, observerHeight, timezone, elements)
st := Limit360(ApparentSiderealTime(jde-timezone/24.0)*15 + observerLon)
hourAngle := st - ra
if hourAngle < 0 {
hourAngle += 360
}
return hourAngle
}
// OrbitCulminationTime 返回轨道目标的中天时刻,输入输出均沿用本仓库现有观测函数的 JD 语义。
func OrbitCulminationTime(jde, observerLon, observerLat, timezone, observerHeight float64, elements OrbitElements) float64 {
jde = math.Floor(jde) + 0.5
estimateJD := jde + Limit360(360-OrbitHourAngle(jde, observerLon, observerLat, timezone, observerHeight, elements))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde float64) float64 {
currentHourAngle := OrbitHourAngle(jde, observerLon, observerLat, timezone, observerHeight, elements)
if currentHourAngle < 180 {
currentHourAngle += 360
}
return currentHourAngle
}
for {
prevJD := estimateJD
hourAngleDelta := normalizedHourAngle(prevJD) - 360
hourAngleSlope := (normalizedHourAngle(prevJD+0.000005) - normalizedHourAngle(prevJD-0.000005)) / 0.00001
estimateJD = prevJD - hourAngleDelta/hourAngleSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return estimateJD
}
// OrbitRiseTime 返回轨道目标在给定当地日期的升起时刻。
func OrbitRiseTime(jde, observerLon, observerLat, timezone, aeroCorrection, observerHeight float64, elements OrbitElements) (float64, error) {
return orbitRiseDown(jde, observerLon, observerLat, timezone, aeroCorrection, observerHeight, elements, true)
}
// OrbitSetTime 返回轨道目标在给定当地日期的落下时刻。
func OrbitSetTime(jde, observerLon, observerLat, timezone, aeroCorrection, observerHeight float64, elements OrbitElements) (float64, error) {
return orbitRiseDown(jde, observerLon, observerLat, timezone, aeroCorrection, observerHeight, elements, false)
}
func orbitRiseDown(jde, observerLon, observerLat, timezone, aeroCorrection, observerHeight float64, elements OrbitElements, isRise bool) (float64, error) {
localTimezone := math.Round(observerLon / 15)
targetAltitude := StandardAltitudePlanet(aeroCorrection, observerHeight, observerLat)
culminationJD := OrbitCulminationTime(jde, observerLon, observerLat, localTimezone, observerHeight, elements)
if OrbitHeight(culminationJD, observerLon, observerLat, localTimezone, observerHeight, elements) < targetAltitude {
return 0, ErrNeverRise
}
if OrbitHeight(culminationJD-0.5, observerLon, observerLat, localTimezone, observerHeight, elements) > targetAltitude {
return 0, ErrNeverSet
}
_, dec, _ := orbitTopocentricObservation(culminationJD, observerLon, observerLat, observerHeight, localTimezone, elements)
cosHourAngle := (Sin(targetAltitude) - Sin(dec)*Sin(observerLat)) / (Cos(dec) * Cos(observerLat))
var eventJD float64
if math.Abs(cosHourAngle) <= 1 {
hourOffset := ArcCos(cosHourAngle) / 15
if isRise {
eventJD = culminationJD - hourOffset/24 - 25.0/24.0/60.0
} else {
eventJD = culminationJD + hourOffset/24 - 25.0/24.0/60.0
}
} else {
eventJD = culminationJD
steps := 0
for OrbitHeight(eventJD, observerLon, observerLat, localTimezone, observerHeight, elements) > targetAltitude {
steps++
if isRise {
eventJD -= 15.0 / 60.0 / 24.0
} else {
eventJD += 15.0 / 60.0 / 24.0
}
if steps > 48 {
break
}
}
}
estimateJD := eventJD
for {
prevJD := estimateJD
altitudeDelta := OrbitHeight(prevJD, observerLon, observerLat, localTimezone, observerHeight, elements) - targetAltitude
altitudeSlope := (OrbitHeight(prevJD+0.000005, observerLon, observerLat, localTimezone, observerHeight, elements) - OrbitHeight(prevJD-0.000005, observerLon, observerLat, localTimezone, observerHeight, elements)) / 0.00001
estimateJD = prevJD - altitudeDelta/altitudeSlope
if math.Abs(estimateJD-prevJD) <= 0.00001 {
break
}
}
return estimateJD - localTimezone/24 + timezone/24, nil
}
+39
View File
@@ -0,0 +1,39 @@
package basic
import . "b612.me/astro/tools"
// OrbitSunDistance 返回轨道目标在给定 TT/TDB 儒略日的日心距离,单位 AU。
func OrbitSunDistance(jd float64, elements OrbitElements) float64 {
_, _, distance := OrbitHeliocentricEclipticJ2000(jd, elements)
return distance
}
// OrbitEarthDistance 返回轨道目标在给定 TT/TDB 儒略日的地心距离,单位 AU。
func OrbitEarthDistance(jd float64, elements OrbitElements) float64 {
_, _, distance := OrbitGeocentricEclipticJ2000(jd, elements)
return distance
}
// OrbitPhaseAngle 返回轨道目标的相位角,单位度。
func OrbitPhaseAngle(jd float64, elements OrbitElements) float64 {
return ArcCos(orbitPhaseCosine(jd, elements))
}
// OrbitIlluminatedFraction 返回轨道目标的被照亮比例。
func OrbitIlluminatedFraction(jd float64, elements OrbitElements) float64 {
return (1 + orbitPhaseCosine(jd, elements)) / 2
}
// OrbitElongation 返回轨道目标相对于太阳的地心视角距,单位度。
func OrbitElongation(jd float64, elements OrbitElements) float64 {
lon, lat, _ := OrbitApparentGeocentricEcliptic(jd, elements)
return StarAngularSeparation(lon, lat, HSunApparentLo(jd), HSunTrueBo(jd))
}
func orbitPhaseCosine(jd float64, elements OrbitElements) float64 {
sunDistance := OrbitSunDistance(jd, elements)
earthDistance := OrbitEarthDistance(jd, elements)
earthSunDistance := EarthAway(jd)
cosine := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
return clampUnit(cosine)
}
+126
View File
@@ -0,0 +1,126 @@
package basic
import (
"math"
"testing"
)
func TestOrbitCircularStateAtEpoch(t *testing.T) {
elements := OrbitElements{
EpochJD: orbitReferenceJD,
A: 2,
E: 0,
I: 0,
Omega: 0,
W: 0,
M0: 0,
}
vector := OrbitHeliocentricXYZJ2000(orbitReferenceJD, elements)
assertSameFloat(t, "x", vector[0], 2)
assertSameFloat(t, "y", vector[1], 0)
assertSameFloat(t, "z", vector[2], 0)
lon, lat, distance := OrbitHeliocentricEclipticJ2000(orbitReferenceJD, elements)
assertSameFloat(t, "lon", lon, 0)
assertSameFloat(t, "lat", lat, 0)
assertSameFloat(t, "distance", distance, 2)
}
func TestOrbitQuarterPeriodOnCircularOrbit(t *testing.T) {
elements := OrbitElements{
EpochJD: orbitReferenceJD,
A: 1,
E: 0,
I: 0,
Omega: 0,
W: 0,
M0: 0,
}
quarterPeriodDays := 90 / OrbitMeanMotion(elements)
vector := OrbitHeliocentricXYZJ2000(orbitReferenceJD+quarterPeriodDays, elements)
if math.Abs(vector[0]) > 1e-10 || math.Abs(vector[1]-1) > 1e-10 || math.Abs(vector[2]) > 1e-10 {
t.Fatalf("quarter-period vector mismatch: got %+v", vector)
}
}
func TestOrbitMeanAndTrueAnomalyCircularMatch(t *testing.T) {
elements := OrbitElements{
EpochJD: orbitReferenceJD,
A: 1.523679,
E: 0,
I: 1.85,
Omega: 49.5,
W: 286.5,
M0: 123.4,
}
jd := orbitReferenceJD + 123.456
meanAnomaly := OrbitMeanAnomaly(jd, elements)
trueAnomaly := OrbitTrueAnomaly(jd, elements)
if math.Abs(meanAnomaly-trueAnomaly) > 1e-12 {
t.Fatalf("circular mean/true anomaly mismatch: mean=%.18f true=%.18f", meanAnomaly, trueAnomaly)
}
}
func TestPerihelionFormMatchesClassicalEllipticState(t *testing.T) {
classical := OrbitElements{
EpochJD: 2457305.5,
A: 3.462249489765068,
E: 0.6409081306555051,
I: 7.040294906760007,
Omega: 50.13557380441372,
W: 12.79824973415729,
M0: 8.859927418758764,
}
perihelion := OrbitElements{
Q: 1.243265641416762,
E: classical.E,
I: classical.I,
Omega: classical.Omega,
W: classical.W,
TpJD: 2457247.588657863465,
}
jd := 2457308.5
classicalVector := OrbitHeliocentricXYZJ2000(jd, classical)
perihelionVector := OrbitHeliocentricXYZJ2000(jd, perihelion)
for i := range classicalVector {
if math.Abs(classicalVector[i]-perihelionVector[i]) > 1e-11 {
t.Fatalf("component %d mismatch: classical=%.18f perihelion=%.18f", i, classicalVector[i], perihelionVector[i])
}
}
}
func TestParabolicPerihelionStateAtTp(t *testing.T) {
elements := OrbitElements{Q: 1, E: 1, I: 0, Omega: 0, W: 0, TpJD: orbitReferenceJD}
vector := OrbitHeliocentricXYZJ2000(orbitReferenceJD, elements)
assertSameFloat(t, "x", vector[0], 1)
assertSameFloat(t, "y", vector[1], 0)
assertSameFloat(t, "z", vector[2], 0)
if !math.IsNaN(OrbitMeanAnomaly(orbitReferenceJD, elements)) {
t.Fatalf("parabolic mean anomaly should be NaN")
}
}
func TestHyperbolicOrbitProducesFiniteState(t *testing.T) {
elements := OrbitElements{Q: 0.5, E: 1.2, I: 12, Omega: 30, W: 45, TpJD: orbitReferenceJD}
vector := OrbitHeliocentricXYZJ2000(orbitReferenceJD+20, elements)
for i, component := range vector {
if math.IsNaN(component) || math.IsInf(component, 0) {
t.Fatalf("component %d not finite: %.18f", i, component)
}
}
}
func TestOrbitInvalidEllipticElementsReturnNaN(t *testing.T) {
elements := OrbitElements{EpochJD: orbitReferenceJD, A: 1, E: 1.1}
if !math.IsNaN(OrbitMeanMotion(elements)) {
t.Fatalf("expected NaN mean motion for invalid elements")
}
vector := OrbitHeliocentricXYZJ2000(orbitReferenceJD, elements)
for i, component := range vector {
if !math.IsNaN(component) {
t.Fatalf("component %d expected NaN, got %.18f", i, component)
}
}
}
+289
View File
@@ -0,0 +1,289 @@
package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
const (
// 月球和内行星轨道角速度更快,取更小的中心差分步长。
orbitalNodeStepFastBodyDays = 0.005
// 外行星保留更稳健的 0.01 day 步长,避免无意义地放大数值噪声。
orbitalNodeStepSlowBodyDays = 0.01
)
// MoonAscendingNode 月球升交点黄经 / ascending node longitude of the Moon.
func MoonAscendingNode(jd float64) float64 {
return MoonAscendingNodeN(jd, -1)
}
// MoonAscendingNodeN 月球升交点黄经(截断版) / truncated ascending node longitude of the Moon.
func MoonAscendingNodeN(jd float64, n int) float64 {
return orbitalAscendingNodeLongitude(jd, n, orbitalNodeStepFastBodyDays, moonGeocentricNodePositionN)
}
// MoonDescendingNode 月球降交点黄经 / descending node longitude of the Moon.
func MoonDescendingNode(jd float64) float64 {
return MoonDescendingNodeN(jd, -1)
}
// MoonDescendingNodeN 月球降交点黄经(截断版) / truncated descending node longitude of the Moon.
func MoonDescendingNodeN(jd float64, n int) float64 {
return Limit360(MoonAscendingNodeN(jd, n) + 180)
}
// MercuryAscendingNode 水星升交点黄经 / ascending node longitude of Mercury.
func MercuryAscendingNode(jd float64) float64 {
return MercuryAscendingNodeN(jd, -1)
}
// MercuryAscendingNodeN 水星升交点黄经(截断版) / truncated ascending node longitude of Mercury.
func MercuryAscendingNodeN(jd float64, n int) float64 {
return planetAscendingNodeLongitudeN(1, jd, n)
}
// MercuryDescendingNode 水星降交点黄经 / descending node longitude of Mercury.
func MercuryDescendingNode(jd float64) float64 {
return MercuryDescendingNodeN(jd, -1)
}
// MercuryDescendingNodeN 水星降交点黄经(截断版) / truncated descending node longitude of Mercury.
func MercuryDescendingNodeN(jd float64, n int) float64 {
return Limit360(MercuryAscendingNodeN(jd, n) + 180)
}
// VenusAscendingNode 金星升交点黄经 / ascending node longitude of Venus.
func VenusAscendingNode(jd float64) float64 {
return VenusAscendingNodeN(jd, -1)
}
// VenusAscendingNodeN 金星升交点黄经(截断版) / truncated ascending node longitude of Venus.
func VenusAscendingNodeN(jd float64, n int) float64 {
return planetAscendingNodeLongitudeN(2, jd, n)
}
// VenusDescendingNode 金星降交点黄经 / descending node longitude of Venus.
func VenusDescendingNode(jd float64) float64 {
return VenusDescendingNodeN(jd, -1)
}
// VenusDescendingNodeN 金星降交点黄经(截断版) / truncated descending node longitude of Venus.
func VenusDescendingNodeN(jd float64, n int) float64 {
return Limit360(VenusAscendingNodeN(jd, n) + 180)
}
// MarsAscendingNode 火星升交点黄经 / ascending node longitude of Mars.
func MarsAscendingNode(jd float64) float64 {
return MarsAscendingNodeN(jd, -1)
}
// MarsAscendingNodeN 火星升交点黄经(截断版) / truncated ascending node longitude of Mars.
func MarsAscendingNodeN(jd float64, n int) float64 {
return planetAscendingNodeLongitudeN(3, jd, n)
}
// MarsDescendingNode 火星降交点黄经 / descending node longitude of Mars.
func MarsDescendingNode(jd float64) float64 {
return MarsDescendingNodeN(jd, -1)
}
// MarsDescendingNodeN 火星降交点黄经(截断版) / truncated descending node longitude of Mars.
func MarsDescendingNodeN(jd float64, n int) float64 {
return Limit360(MarsAscendingNodeN(jd, n) + 180)
}
// JupiterAscendingNode 木星升交点黄经 / ascending node longitude of Jupiter.
func JupiterAscendingNode(jd float64) float64 {
return JupiterAscendingNodeN(jd, -1)
}
// JupiterAscendingNodeN 木星升交点黄经(截断版) / truncated ascending node longitude of Jupiter.
func JupiterAscendingNodeN(jd float64, n int) float64 {
return planetAscendingNodeLongitudeN(4, jd, n)
}
// JupiterDescendingNode 木星降交点黄经 / descending node longitude of Jupiter.
func JupiterDescendingNode(jd float64) float64 {
return JupiterDescendingNodeN(jd, -1)
}
// JupiterDescendingNodeN 木星降交点黄经(截断版) / truncated descending node longitude of Jupiter.
func JupiterDescendingNodeN(jd float64, n int) float64 {
return Limit360(JupiterAscendingNodeN(jd, n) + 180)
}
// SaturnAscendingNode 土星升交点黄经 / ascending node longitude of Saturn.
func SaturnAscendingNode(jd float64) float64 {
return SaturnAscendingNodeN(jd, -1)
}
// SaturnAscendingNodeN 土星升交点黄经(截断版) / truncated ascending node longitude of Saturn.
func SaturnAscendingNodeN(jd float64, n int) float64 {
return planetAscendingNodeLongitudeN(5, jd, n)
}
// SaturnDescendingNode 土星降交点黄经 / descending node longitude of Saturn.
func SaturnDescendingNode(jd float64) float64 {
return SaturnDescendingNodeN(jd, -1)
}
// SaturnDescendingNodeN 土星降交点黄经(截断版) / truncated descending node longitude of Saturn.
func SaturnDescendingNodeN(jd float64, n int) float64 {
return Limit360(SaturnAscendingNodeN(jd, n) + 180)
}
// UranusAscendingNode 天王星升交点黄经 / ascending node longitude of Uranus.
func UranusAscendingNode(jd float64) float64 {
return UranusAscendingNodeN(jd, -1)
}
// UranusAscendingNodeN 天王星升交点黄经(截断版) / truncated ascending node longitude of Uranus.
func UranusAscendingNodeN(jd float64, n int) float64 {
return planetAscendingNodeLongitudeN(6, jd, n)
}
// UranusDescendingNode 天王星降交点黄经 / descending node longitude of Uranus.
func UranusDescendingNode(jd float64) float64 {
return UranusDescendingNodeN(jd, -1)
}
// UranusDescendingNodeN 天王星降交点黄经(截断版) / truncated descending node longitude of Uranus.
func UranusDescendingNodeN(jd float64, n int) float64 {
return Limit360(UranusAscendingNodeN(jd, n) + 180)
}
// NeptuneAscendingNode 海王星升交点黄经 / ascending node longitude of Neptune.
func NeptuneAscendingNode(jd float64) float64 {
return NeptuneAscendingNodeN(jd, -1)
}
// NeptuneAscendingNodeN 海王星升交点黄经(截断版) / truncated ascending node longitude of Neptune.
func NeptuneAscendingNodeN(jd float64, n int) float64 {
return planetAscendingNodeLongitudeN(7, jd, n)
}
// NeptuneDescendingNode 海王星降交点黄经 / descending node longitude of Neptune.
func NeptuneDescendingNode(jd float64) float64 {
return NeptuneDescendingNodeN(jd, -1)
}
// NeptuneDescendingNodeN 海王星降交点黄经(截断版) / truncated descending node longitude of Neptune.
func NeptuneDescendingNodeN(jd float64, n int) float64 {
return Limit360(NeptuneAscendingNodeN(jd, n) + 180)
}
func planetAscendingNodeLongitudeN(planetIndex int, jd float64, n int) float64 {
step := orbitalNodeStepSlowBodyDays
if planetIndex <= 3 {
step = orbitalNodeStepFastBodyDays
}
return orbitalAscendingNodeLongitude(jd, n, step, func(sampleJD float64, seriesTerms int) Vector3 {
return planetHeliocentricNodePositionN(planetIndex, sampleJD, seriesTerms)
})
}
func orbitalAscendingNodeLongitude(jd float64, n int, step float64, position func(float64, int) Vector3) float64 {
current := eclipticVectorAtReferenceEpoch(position(jd, n), jd, jd)
previous := eclipticVectorAtReferenceEpoch(position(jd-step, n), jd-step, jd)
next := eclipticVectorAtReferenceEpoch(position(jd+step, n), jd+step, jd)
velocity := Vector3{
(next[0] - previous[0]) / (2 * step),
(next[1] - previous[1]) / (2 * step),
(next[2] - previous[2]) / (2 * step),
}
angularMomentum := pxp(current, velocity)
nodeVector, magnitude := pn(Vector3{-angularMomentum[1], angularMomentum[0], 0})
if magnitude == 0 {
return 0
}
return Limit360(math.Atan2(nodeVector[1], nodeVector[0]) * deg)
}
func eclipticVectorAtReferenceEpoch(vector Vector3, sampleJD, referenceJD float64) Vector3 {
if sampleJD == referenceJD {
return vector
}
sampleEquatorial := rotateEclipticToEquatorial(vector, EclipticObliquity(sampleJD, false))
precessedEquatorial := applyMatrix3(precessionMatrix(sampleJD, referenceJD), sampleEquatorial)
return rotateEquatorialToEcliptic(precessedEquatorial, EclipticObliquity(referenceJD, false))
}
func rotateEclipticToEquatorial(vector Vector3, obliquity float64) Vector3 {
epsilon := obliquity * rad
cosEpsilon := math.Cos(epsilon)
sinEpsilon := math.Sin(epsilon)
return Vector3{
vector[0],
vector[1]*cosEpsilon - vector[2]*sinEpsilon,
vector[1]*sinEpsilon + vector[2]*cosEpsilon,
}
}
func rotateEquatorialToEcliptic(vector Vector3, obliquity float64) Vector3 {
epsilon := obliquity * rad
cosEpsilon := math.Cos(epsilon)
sinEpsilon := math.Sin(epsilon)
return Vector3{
vector[0],
vector[1]*cosEpsilon + vector[2]*sinEpsilon,
-vector[1]*sinEpsilon + vector[2]*cosEpsilon,
}
}
func precessionMatrix(jdFrom, jdTo float64) Matrix3 {
epjFrom := 2000.0 + (jdFrom-2451545.0)/365.25
epjTo := 2000.0 + (jdTo-2451545.0)/365.25
rpFrom := ltpPMAT(epjFrom)
rpFromInv := Matrix3{
{rpFrom[0][0], rpFrom[1][0], rpFrom[2][0]},
{rpFrom[0][1], rpFrom[1][1], rpFrom[2][1]},
{rpFrom[0][2], rpFrom[1][2], rpFrom[2][2]},
}
rpTo := ltpPMAT(epjTo)
var result Matrix3
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
for k := 0; k < 3; k++ {
result[i][j] += rpTo[i][k] * rpFromInv[k][j]
}
}
}
return result
}
func applyMatrix3(matrix Matrix3, vector Vector3) Vector3 {
return Vector3{
matrix[0][0]*vector[0] + matrix[0][1]*vector[1] + matrix[0][2]*vector[2],
matrix[1][0]*vector[0] + matrix[1][1]*vector[1] + matrix[1][2]*vector[2],
matrix[2][0]*vector[0] + matrix[2][1]*vector[1] + matrix[2][2]*vector[2],
}
}
func planetHeliocentricNodePositionN(planetIndex int, jd float64, n int) Vector3 {
longitude := planet.WherePlanetN(planetIndex, 0, jd, n)
latitude := planet.WherePlanetN(planetIndex, 1, jd, n)
radius := planet.WherePlanetN(planetIndex, 2, jd, n)
return eclipticCartesian(longitude, latitude, radius)
}
func moonGeocentricNodePositionN(jd float64, n int) Vector3 {
longitude := HMoonTrueLoN(jd, n)
latitude := HMoonTrueBoN(jd, n)
radius := HMoonAwayN(jd, n)
return eclipticCartesian(longitude, latitude, radius)
}
func eclipticCartesian(longitude, latitude, radius float64) Vector3 {
cosLatitude := Cos(latitude)
return Vector3{
radius * cosLatitude * Cos(longitude),
radius * cosLatitude * Sin(longitude),
radius * Sin(latitude),
}
}
+77
View File
@@ -0,0 +1,77 @@
package basic
import (
"math"
"testing"
)
func TestOrbitalNodesDescendingOpposesAscending(t *testing.T) {
jde := 2461157.5
testCases := []struct {
name string
ascending func(float64) float64
descending func(float64) float64
}{
{name: "moon", ascending: MoonAscendingNode, descending: MoonDescendingNode},
{name: "mercury", ascending: MercuryAscendingNode, descending: MercuryDescendingNode},
{name: "venus", ascending: VenusAscendingNode, descending: VenusDescendingNode},
{name: "mars", ascending: MarsAscendingNode, descending: MarsDescendingNode},
{name: "jupiter", ascending: JupiterAscendingNode, descending: JupiterDescendingNode},
{name: "saturn", ascending: SaturnAscendingNode, descending: SaturnDescendingNode},
{name: "uranus", ascending: UranusAscendingNode, descending: UranusDescendingNode},
{name: "neptune", ascending: NeptuneAscendingNode, descending: NeptuneDescendingNode},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ascending := tc.ascending(jde)
descending := tc.descending(jde)
want := ascending + 180
if want >= 360 {
want -= 360
}
if diff := angularDifference(descending, ascending+180); diff > 1e-10 {
t.Fatalf("descending node mismatch: got %.12f want %.12f diff=%.12g", descending, want, diff)
}
})
}
}
func TestOrbitalNodesNFullMatchesDefault(t *testing.T) {
jde := 2461157.5
testCases := []struct {
name string
defaultFn func(float64) float64
truncatedN func(float64, int) float64
}{
{name: "moon", defaultFn: MoonAscendingNode, truncatedN: MoonAscendingNodeN},
{name: "mercury", defaultFn: MercuryAscendingNode, truncatedN: MercuryAscendingNodeN},
{name: "venus", defaultFn: VenusAscendingNode, truncatedN: VenusAscendingNodeN},
{name: "mars", defaultFn: MarsAscendingNode, truncatedN: MarsAscendingNodeN},
{name: "jupiter", defaultFn: JupiterAscendingNode, truncatedN: JupiterAscendingNodeN},
{name: "saturn", defaultFn: SaturnAscendingNode, truncatedN: SaturnAscendingNodeN},
{name: "uranus", defaultFn: UranusAscendingNode, truncatedN: UranusAscendingNodeN},
{name: "neptune", defaultFn: NeptuneAscendingNode, truncatedN: NeptuneAscendingNodeN},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := tc.defaultFn(jde)
gotN := tc.truncatedN(jde, -1)
if diff := angularDifference(got, gotN); diff > 1e-10 {
t.Fatalf("full-series N mismatch: got %.12f want %.12f diff=%.12g", gotN, got, diff)
}
})
}
}
func angularDifference(a, b float64) float64 {
diff := math.Mod(a-b, 360)
if diff < -180 {
diff += 360
}
if diff > 180 {
diff -= 360
}
return math.Abs(diff)
}
@@ -0,0 +1,30 @@
package basic
import "testing"
func benchmarkOuterPlanetEventFamily(b *testing.B, plan outerPlanetEventPlan, cases []outerPlanetEventCase) {
samples := plan.samples()
var sink float64
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, sample := range samples {
jd := outerPlanetEventSampleTTJD(sample)
for _, event := range cases {
sink += event.fn(jd)
}
}
}
_ = sink
}
func BenchmarkOuterPlanetEventFamilies(b *testing.B) {
for _, plan := range outerPlanetEventPlans() {
plan := plan
b.Run(plan.planet+"PhaseFamily", func(b *testing.B) {
benchmarkOuterPlanetEventFamily(b, plan, plan.phaseCases)
})
b.Run(plan.planet+"RetrogradeFamily", func(b *testing.B) {
benchmarkOuterPlanetEventFamily(b, plan, plan.retroCases)
})
}
}
+55
View File
@@ -0,0 +1,55 @@
package basic
import (
"testing"
"time"
)
func TestOuterPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
cases := []struct {
name string
seed float64
lastFn func(float64) float64
nextFn func(float64) float64
}{
{name: "JupiterConjunction", seed: NextJupiterConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterConjunction, nextFn: NextJupiterConjunction},
{name: "JupiterOpposition", seed: NextJupiterOpposition(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterOpposition, nextFn: NextJupiterOpposition},
{name: "JupiterEasternQuadrature", seed: NextJupiterEasternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterEasternQuadrature, nextFn: NextJupiterEasternQuadrature},
{name: "JupiterWesternQuadrature", seed: NextJupiterWesternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterWesternQuadrature, nextFn: NextJupiterWesternQuadrature},
{name: "JupiterP2R", seed: NextJupiterProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterProgradeToRetrograde, nextFn: NextJupiterProgradeToRetrograde},
{name: "JupiterR2P", seed: NextJupiterRetrogradeToPrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterRetrogradeToPrograde, nextFn: NextJupiterRetrogradeToPrograde},
{name: "SaturnOpposition", seed: NextSaturnOpposition(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastSaturnOpposition, nextFn: NextSaturnOpposition},
{name: "SaturnP2R", seed: NextSaturnProgradeToRetrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastSaturnProgradeToRetrograde, nextFn: NextSaturnProgradeToRetrograde},
{name: "SaturnR2P", seed: NextSaturnRetrogradeToPrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastSaturnRetrogradeToPrograde, nextFn: NextSaturnRetrogradeToPrograde},
{name: "UranusOpposition", seed: NextUranusOpposition(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastUranusOpposition, nextFn: NextUranusOpposition},
{name: "UranusP2R", seed: NextUranusProgradeToRetrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastUranusProgradeToRetrograde, nextFn: NextUranusProgradeToRetrograde},
{name: "UranusR2P", seed: NextUranusRetrogradeToPrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastUranusRetrogradeToPrograde, nextFn: NextUranusRetrogradeToPrograde},
{name: "NeptuneOpposition", seed: NextNeptuneOpposition(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastNeptuneOpposition, nextFn: NextNeptuneOpposition},
{name: "NeptuneP2R", seed: NextNeptuneProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastNeptuneProgradeToRetrograde, nextFn: NextNeptuneProgradeToRetrograde},
{name: "NeptuneR2P", seed: NextNeptuneRetrogradeToPrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastNeptuneRetrogradeToPrograde, nextFn: NextNeptuneRetrogradeToPrograde},
{name: "MarsConjunction", seed: NextMarsConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsConjunction, nextFn: NextMarsConjunction},
{name: "MarsOpposition", seed: NextMarsOpposition(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsOpposition, nextFn: NextMarsOpposition},
{name: "MarsEasternQuadrature", seed: NextMarsEasternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsEasternQuadrature, nextFn: NextMarsEasternQuadrature},
{name: "MarsWesternQuadrature", seed: NextMarsWesternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsWesternQuadrature, nextFn: NextMarsWesternQuadrature},
{name: "MarsP2R", seed: NextMarsProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsProgradeToRetrograde, nextFn: NextMarsProgradeToRetrograde},
{name: "MarsR2P", seed: NextMarsRetrogradeToPrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsRetrogradeToPrograde, nextFn: NextMarsRetrogradeToPrograde},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
queryTT := TD2UT(tc.seed, true)
last := tc.lastFn(queryTT)
next := tc.nextFn(queryTT)
if !sameEventJD(last, tc.seed) {
t.Fatalf("last exact boundary mismatch: got %.12f want %.12f", last, tc.seed)
}
if !sameEventJD(next, tc.seed) {
t.Fatalf("next exact boundary mismatch: got %.12f want %.12f", next, tc.seed)
}
})
}
}
func ttjdUTC(year, month, day, hour, min, sec int) float64 {
return TD2UT(Date2JDE(time.Date(year, time.Month(month), day, hour, min, sec, 0, time.UTC)), true)
}
+193
View File
@@ -0,0 +1,193 @@
package basic
import (
"time"
)
type outerPlanetEventFunc func(float64) float64
type outerPlanetEventCase struct {
name string
tolerance float64
fn outerPlanetEventFunc
}
type outerPlanetEventPlan struct {
planet string
baselineFile string
samples func() []time.Time
phaseCases []outerPlanetEventCase
retroCases []outerPlanetEventCase
}
func (plan outerPlanetEventPlan) allCases() []outerPlanetEventCase {
cases := make([]outerPlanetEventCase, 0, len(plan.phaseCases)+len(plan.retroCases))
cases = append(cases, plan.phaseCases...)
cases = append(cases, plan.retroCases...)
return cases
}
func outerPlanetEventSampleTTJD(date time.Time) float64 {
return TD2UT(Date2JDE(date.UTC()), true)
}
func jupiterEventSamples() []time.Time {
start := time.Date(1991, 3, 11, 5, 17, 23, 456000000, time.UTC)
samples := make([]time.Time, 0, 96)
for i := 0; i < 48; i++ {
d := start.AddDate(0, 0, i*227)
d = d.Add(time.Duration((i%8)*4)*time.Hour + time.Duration((i%11)*9)*time.Minute + time.Duration((i%13)*17)*time.Second)
samples = append(samples, d)
}
extraStart := start.AddDate(0, 0, 37)
for i := 0; i < 48; i++ {
d := extraStart.AddDate(0, 0, i*149)
d = d.Add(time.Duration((i%6)*7)*time.Hour + time.Duration((i%10)*13)*time.Minute + time.Duration((i%15)*23)*time.Second)
samples = append(samples, d)
}
return samples
}
func saturnEventSamples() []time.Time {
start := time.Date(1990, 7, 21, 8, 12, 34, 567000000, time.UTC)
samples := make([]time.Time, 0, 96)
for i := 0; i < 48; i++ {
d := start.AddDate(0, 0, i*233)
d = d.Add(time.Duration((i%9)*5)*time.Hour + time.Duration((i%10)*7)*time.Minute + time.Duration((i%12)*19)*time.Second)
samples = append(samples, d)
}
extraStart := start.AddDate(0, 0, 43)
for i := 0; i < 48; i++ {
d := extraStart.AddDate(0, 0, i*157)
d = d.Add(time.Duration((i%7)*6)*time.Hour + time.Duration((i%9)*11)*time.Minute + time.Duration((i%14)*29)*time.Second)
samples = append(samples, d)
}
return samples
}
func uranusEventSamples() []time.Time {
start := time.Date(1993, 11, 5, 10, 22, 33, 444000000, time.UTC)
samples := make([]time.Time, 0, 96)
for i := 0; i < 48; i++ {
d := start.AddDate(0, 0, i*239)
d = d.Add(time.Duration((i%7)*6)*time.Hour + time.Duration((i%9)*13)*time.Minute + time.Duration((i%14)*11)*time.Second)
samples = append(samples, d)
}
extraStart := start.AddDate(0, 0, 59)
for i := 0; i < 48; i++ {
d := extraStart.AddDate(0, 0, i*163)
d = d.Add(time.Duration((i%8)*5)*time.Hour + time.Duration((i%12)*17)*time.Minute + time.Duration((i%13)*31)*time.Second)
samples = append(samples, d)
}
return samples
}
func neptuneEventSamples() []time.Time {
start := time.Date(1996, 4, 17, 3, 14, 15, 926000000, time.UTC)
samples := make([]time.Time, 0, 96)
for i := 0; i < 48; i++ {
d := start.AddDate(0, 0, i*241)
d = d.Add(time.Duration((i%10)*3)*time.Hour + time.Duration((i%8)*17)*time.Minute + time.Duration((i%15)*7)*time.Second)
samples = append(samples, d)
}
extraStart := start.AddDate(0, 0, 67)
for i := 0; i < 48; i++ {
d := extraStart.AddDate(0, 0, i*167)
d = d.Add(time.Duration((i%9)*4)*time.Hour + time.Duration((i%11)*19)*time.Minute + time.Duration((i%14)*27)*time.Second)
samples = append(samples, d)
}
return samples
}
func outerPlanetEventPlans() []outerPlanetEventPlan {
const (
conjunctionTolerance = 0.00001
searchTolerance = 30.0 / 86400.0
)
return []outerPlanetEventPlan{
{
planet: "Jupiter",
baselineFile: "testdata/jupiter_event_baseline.json",
samples: jupiterEventSamples,
phaseCases: []outerPlanetEventCase{
{name: "LastJupiterConjunction", tolerance: conjunctionTolerance, fn: LastJupiterConjunction},
{name: "NextJupiterConjunction", tolerance: conjunctionTolerance, fn: NextJupiterConjunction},
{name: "LastJupiterOpposition", tolerance: conjunctionTolerance, fn: LastJupiterOpposition},
{name: "NextJupiterOpposition", tolerance: conjunctionTolerance, fn: NextJupiterOpposition},
{name: "LastJupiterEasternQuadrature", tolerance: conjunctionTolerance, fn: LastJupiterEasternQuadrature},
{name: "NextJupiterEasternQuadrature", tolerance: conjunctionTolerance, fn: NextJupiterEasternQuadrature},
{name: "LastJupiterWesternQuadrature", tolerance: conjunctionTolerance, fn: LastJupiterWesternQuadrature},
{name: "NextJupiterWesternQuadrature", tolerance: conjunctionTolerance, fn: NextJupiterWesternQuadrature},
},
retroCases: []outerPlanetEventCase{
{name: "LastJupiterProgradeToRetrograde", tolerance: searchTolerance, fn: LastJupiterProgradeToRetrograde},
{name: "NextJupiterProgradeToRetrograde", tolerance: searchTolerance, fn: NextJupiterProgradeToRetrograde},
{name: "LastJupiterRetrogradeToPrograde", tolerance: searchTolerance, fn: LastJupiterRetrogradeToPrograde},
{name: "NextJupiterRetrogradeToPrograde", tolerance: searchTolerance, fn: NextJupiterRetrogradeToPrograde},
},
},
{
planet: "Saturn",
baselineFile: "testdata/saturn_event_baseline.json",
samples: saturnEventSamples,
phaseCases: []outerPlanetEventCase{
{name: "LastSaturnConjunction", tolerance: conjunctionTolerance, fn: LastSaturnConjunction},
{name: "NextSaturnConjunction", tolerance: conjunctionTolerance, fn: NextSaturnConjunction},
{name: "LastSaturnOpposition", tolerance: conjunctionTolerance, fn: LastSaturnOpposition},
{name: "NextSaturnOpposition", tolerance: conjunctionTolerance, fn: NextSaturnOpposition},
{name: "LastSaturnEasternQuadrature", tolerance: conjunctionTolerance, fn: LastSaturnEasternQuadrature},
{name: "NextSaturnEasternQuadrature", tolerance: conjunctionTolerance, fn: NextSaturnEasternQuadrature},
{name: "LastSaturnWesternQuadrature", tolerance: conjunctionTolerance, fn: LastSaturnWesternQuadrature},
{name: "NextSaturnWesternQuadrature", tolerance: conjunctionTolerance, fn: NextSaturnWesternQuadrature},
},
retroCases: []outerPlanetEventCase{
{name: "LastSaturnProgradeToRetrograde", tolerance: searchTolerance, fn: LastSaturnProgradeToRetrograde},
{name: "NextSaturnProgradeToRetrograde", tolerance: searchTolerance, fn: NextSaturnProgradeToRetrograde},
{name: "LastSaturnRetrogradeToPrograde", tolerance: searchTolerance, fn: LastSaturnRetrogradeToPrograde},
{name: "NextSaturnRetrogradeToPrograde", tolerance: searchTolerance, fn: NextSaturnRetrogradeToPrograde},
},
},
{
planet: "Uranus",
baselineFile: "testdata/uranus_event_baseline.json",
samples: uranusEventSamples,
phaseCases: []outerPlanetEventCase{
{name: "LastUranusConjunction", tolerance: conjunctionTolerance, fn: LastUranusConjunction},
{name: "NextUranusConjunction", tolerance: conjunctionTolerance, fn: NextUranusConjunction},
{name: "LastUranusOpposition", tolerance: conjunctionTolerance, fn: LastUranusOpposition},
{name: "NextUranusOpposition", tolerance: conjunctionTolerance, fn: NextUranusOpposition},
{name: "LastUranusEasternQuadrature", tolerance: conjunctionTolerance, fn: LastUranusEasternQuadrature},
{name: "NextUranusEasternQuadrature", tolerance: conjunctionTolerance, fn: NextUranusEasternQuadrature},
{name: "LastUranusWesternQuadrature", tolerance: conjunctionTolerance, fn: LastUranusWesternQuadrature},
{name: "NextUranusWesternQuadrature", tolerance: conjunctionTolerance, fn: NextUranusWesternQuadrature},
},
retroCases: []outerPlanetEventCase{
{name: "LastUranusProgradeToRetrograde", tolerance: searchTolerance, fn: LastUranusProgradeToRetrograde},
{name: "NextUranusProgradeToRetrograde", tolerance: searchTolerance, fn: NextUranusProgradeToRetrograde},
{name: "LastUranusRetrogradeToPrograde", tolerance: searchTolerance, fn: LastUranusRetrogradeToPrograde},
{name: "NextUranusRetrogradeToPrograde", tolerance: searchTolerance, fn: NextUranusRetrogradeToPrograde},
},
},
{
planet: "Neptune",
baselineFile: "testdata/neptune_event_baseline.json",
samples: neptuneEventSamples,
phaseCases: []outerPlanetEventCase{
{name: "LastNeptuneConjunction", tolerance: conjunctionTolerance, fn: LastNeptuneConjunction},
{name: "NextNeptuneConjunction", tolerance: conjunctionTolerance, fn: NextNeptuneConjunction},
{name: "LastNeptuneOpposition", tolerance: conjunctionTolerance, fn: LastNeptuneOpposition},
{name: "NextNeptuneOpposition", tolerance: conjunctionTolerance, fn: NextNeptuneOpposition},
{name: "LastNeptuneEasternQuadrature", tolerance: conjunctionTolerance, fn: LastNeptuneEasternQuadrature},
{name: "NextNeptuneEasternQuadrature", tolerance: conjunctionTolerance, fn: NextNeptuneEasternQuadrature},
{name: "LastNeptuneWesternQuadrature", tolerance: conjunctionTolerance, fn: LastNeptuneWesternQuadrature},
{name: "NextNeptuneWesternQuadrature", tolerance: conjunctionTolerance, fn: NextNeptuneWesternQuadrature},
},
retroCases: []outerPlanetEventCase{
{name: "LastNeptuneProgradeToRetrograde", tolerance: searchTolerance, fn: LastNeptuneProgradeToRetrograde},
{name: "NextNeptuneProgradeToRetrograde", tolerance: searchTolerance, fn: NextNeptuneProgradeToRetrograde},
{name: "LastNeptuneRetrogradeToPrograde", tolerance: searchTolerance, fn: LastNeptuneRetrogradeToPrograde},
{name: "NextNeptuneRetrogradeToPrograde", tolerance: searchTolerance, fn: NextNeptuneRetrogradeToPrograde},
},
},
}
}
@@ -0,0 +1,61 @@
package basic
import (
"encoding/json"
"math"
"os"
"testing"
)
type outerPlanetEventBaselineSample struct {
InputUTC string `json:"input_utc"`
TTJDBits uint64 `json:"tt_jd_bits"`
Events map[string]uint64 `json:"events"`
}
type outerPlanetEventBaseline struct {
Samples []outerPlanetEventBaselineSample `json:"samples"`
}
func loadOuterPlanetEventBaseline(t *testing.T, path string) outerPlanetEventBaseline {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var baseline outerPlanetEventBaseline
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatal(err)
}
if len(baseline.Samples) == 0 {
t.Fatalf("empty baseline: %s", path)
}
return baseline
}
func TestOuterPlanetEventBaselineRegression(t *testing.T) {
for _, plan := range outerPlanetEventPlans() {
t.Run(plan.planet, func(t *testing.T) {
baseline := loadOuterPlanetEventBaseline(t, plan.baselineFile)
cases := plan.allCases()
for _, sample := range baseline.Samples {
jd := math.Float64frombits(sample.TTJDBits)
for _, event := range cases {
wantBits, ok := sample.Events[event.name]
if !ok {
t.Fatalf("%s missing baseline event %s", sample.InputUTC, event.name)
}
want := math.Float64frombits(wantBits)
got := event.fn(jd)
diff := math.Abs(got - want)
if diff > event.tolerance {
t.Fatalf("%s %s diff %.12f > tolerance %.12f", sample.InputUTC, event.name, diff, event.tolerance)
}
}
}
})
}
}
+181
View File
@@ -0,0 +1,181 @@
package basic
import (
"encoding/json"
"os"
"strings"
"testing"
"time"
)
type outerTruthBaselineFile struct {
Events []outerTruthBaselineEvent `json:"events"`
}
type outerTruthBaselineEvent struct {
Planet string `json:"planet"`
Kind string `json:"kind"`
HintKind string `json:"hint_kind"`
NAOJHintJST string `json:"naoj_hint_jst"`
Precision string `json:"precision"`
CandidateJST string `json:"candidate_jst"`
VerifiedJST string `json:"verified_jst"`
CandidateSource string `json:"candidate_source"`
}
func loadOuterTruthBaseline(t *testing.T) outerTruthBaselineFile {
t.Helper()
paths := [][]string{
{
"testdata/jpl_outer_event_baseline.json",
"basic/testdata/jpl_outer_event_baseline.json",
},
{
"testdata/jpl_outer_event_baseline_21c_sample.json",
"basic/testdata/jpl_outer_event_baseline_21c_sample.json",
},
}
var merged outerTruthBaselineFile
for index, candidates := range paths {
var (
data []byte
err error
)
for _, path := range candidates {
data, err = os.ReadFile(path)
if err == nil {
var baseline outerTruthBaselineFile
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatal(err)
}
merged.Events = append(merged.Events, baseline.Events...)
break
}
}
if err != nil && index == 0 {
t.Fatal(err)
}
}
if len(merged.Events) == 0 {
t.Fatal("empty outer truth baseline file")
}
return merged
}
func outerTruthTolerance(event outerTruthBaselineEvent) time.Duration {
switch event.Kind {
case "CONJ", "OPP", "EQE", "EQW":
return 2 * time.Minute
default:
return 2 * time.Minute
}
}
func outerTruthEventFuncs(t *testing.T, event outerTruthBaselineEvent) (func(float64) float64, func(float64) float64) {
t.Helper()
switch event.Planet + ":" + event.Kind {
case "Jupiter:CONJ":
return LastJupiterConjunction, NextJupiterConjunction
case "Jupiter:OPP":
return LastJupiterOpposition, NextJupiterOpposition
case "Jupiter:EQE":
return LastJupiterEasternQuadrature, NextJupiterEasternQuadrature
case "Jupiter:EQW":
return LastJupiterWesternQuadrature, NextJupiterWesternQuadrature
case "Jupiter:P2R":
return LastJupiterProgradeToRetrograde, NextJupiterProgradeToRetrograde
case "Jupiter:R2P":
return LastJupiterRetrogradeToPrograde, NextJupiterRetrogradeToPrograde
case "Saturn:CONJ":
return LastSaturnConjunction, NextSaturnConjunction
case "Saturn:OPP":
return LastSaturnOpposition, NextSaturnOpposition
case "Saturn:EQE":
return LastSaturnEasternQuadrature, NextSaturnEasternQuadrature
case "Saturn:EQW":
return LastSaturnWesternQuadrature, NextSaturnWesternQuadrature
case "Saturn:P2R":
return LastSaturnProgradeToRetrograde, NextSaturnProgradeToRetrograde
case "Saturn:R2P":
return LastSaturnRetrogradeToPrograde, NextSaturnRetrogradeToPrograde
case "Uranus:CONJ":
return LastUranusConjunction, NextUranusConjunction
case "Uranus:OPP":
return LastUranusOpposition, NextUranusOpposition
case "Uranus:EQE":
return LastUranusEasternQuadrature, NextUranusEasternQuadrature
case "Uranus:EQW":
return LastUranusWesternQuadrature, NextUranusWesternQuadrature
case "Uranus:P2R":
return LastUranusProgradeToRetrograde, NextUranusProgradeToRetrograde
case "Uranus:R2P":
return LastUranusRetrogradeToPrograde, NextUranusRetrogradeToPrograde
case "Neptune:CONJ":
return LastNeptuneConjunction, NextNeptuneConjunction
case "Neptune:OPP":
return LastNeptuneOpposition, NextNeptuneOpposition
case "Neptune:EQE":
return LastNeptuneEasternQuadrature, NextNeptuneEasternQuadrature
case "Neptune:EQW":
return LastNeptuneWesternQuadrature, NextNeptuneWesternQuadrature
case "Neptune:P2R":
return LastNeptuneProgradeToRetrograde, NextNeptuneProgradeToRetrograde
case "Neptune:R2P":
return LastNeptuneRetrogradeToPrograde, NextNeptuneRetrogradeToPrograde
default:
t.Fatalf("unsupported outer event %s:%s", event.Planet, event.Kind)
return nil, nil
}
}
func assertOuterTruthBaselineEvent(t *testing.T, event outerTruthBaselineEvent, lastFn, nextFn func(float64) float64) {
t.Helper()
when := parseInnerBaselineTime(t, event.VerifiedJST)
before := when.Add(-7 * 24 * time.Hour)
after := when.Add(7 * 24 * time.Hour)
next := JDE2DateByZone(nextFn(toUTJD(before)), when.Location(), false)
last := JDE2DateByZone(lastFn(toUTJD(after)), when.Location(), false)
tolerance := outerTruthTolerance(event)
if diff := next.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s next mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, next, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
if diff := last.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s last mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, last, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
}
func TestOuterPlanetPhaseTruthAgainstJPL(t *testing.T) {
baseline := loadOuterTruthBaseline(t)
for _, event := range baseline.Events {
event := event
switch event.Kind {
case "P2R", "R2P":
// Station rows are retained as JPL apparent-RA reference data for
// future refinement. Current station behavior is constrained by the
// library's existing station baseline instead of these reference rows.
continue
}
name := strings.Join([]string{event.Planet, event.Kind, event.VerifiedJST}, "_")
t.Run(name, func(t *testing.T) {
lastFn, nextFn := outerTruthEventFuncs(t, event)
assertOuterTruthBaselineEvent(t, event, lastFn, nextFn)
})
}
}
func TestOuterPlanetStationJPLReferenceLoaded(t *testing.T) {
baseline := loadOuterTruthBaseline(t)
count := 0
for _, event := range baseline.Events {
switch event.Kind {
case "P2R", "R2P":
count++
}
}
if count == 0 {
t.Fatal("missing outer station JPL reference rows")
}
}
+25
View File
@@ -0,0 +1,25 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
// ParallacticAngleByHourAngle 返回视差角(天顶方向角)/ parallactic angle.
//
// hourAngle 为目标时角,dec 为赤纬,lat 为观测者纬度,单位均为度。
// 返回值是 atan2 公式给出的有符号结果,范围通常为 [-180, 180] 度。
// hourAngle may be signed or normalized to [0, 360); the trigonometric
// formula handles either representation.
func ParallacticAngleByHourAngle(hourAngle, dec, lat float64) float64 {
return math.Atan2(
Sin(hourAngle),
Tan(lat)*Cos(dec)-Sin(dec)*Cos(hourAngle),
) * 180 / math.Pi
}
// StarParallacticAngle 返回星体在给定观测条件下的视差角(天顶方向角)/ parallactic angle.
func StarParallacticAngle(jde, ra, dec, lon, lat, timezone float64) float64 {
return ParallacticAngleByHourAngle(StarHourAngle(jde, ra, lon, timezone), dec, lat)
}
+45
View File
@@ -0,0 +1,45 @@
package basic
import (
"math"
"testing"
"time"
)
func TestParallacticAngleByHourAngleKnownCases(t *testing.T) {
cases := []struct {
name string
hourAngle float64
dec float64
lat float64
want float64
}{
{name: "meridian", hourAngle: 0, dec: 10, lat: 45, want: 0},
{name: "equator west", hourAngle: 30, dec: 0, lat: 0, want: 90},
{name: "equator east", hourAngle: -30, dec: 0, lat: 0, want: -90},
}
for _, tc := range cases {
got := ParallacticAngleByHourAngle(tc.hourAngle, tc.dec, tc.lat)
if math.Abs(got-tc.want) > 1e-12 {
t.Fatalf("%s mismatch: got %.15f want %.15f", tc.name, got, tc.want)
}
}
}
func TestStarParallacticAngleMatchesHourAngleForm(t *testing.T) {
date := time.Date(2026, 4, 29, 21, 15, 0, 0, time.FixedZone("CST", 8*3600))
jde := Date2JDE(date)
_, offsetSeconds := date.Zone()
timezone := float64(offsetSeconds) / 3600.0
ra := 101.28715533
dec := -16.71611586
lon := 115.0
lat := 40.0
got := StarParallacticAngle(jde, ra, dec, lon, lat, timezone)
want := ParallacticAngleByHourAngle(StarHourAngle(jde, ra, lon, timezone), dec, lat)
if math.Abs(got-want) > 1e-12 {
t.Fatalf("star parallactic angle mismatch: got %.15f want %.15f", got, want)
}
}
+84
View File
@@ -0,0 +1,84 @@
package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
type planetGeocentricPosition struct {
x float64
y float64
z float64
lo float64
bo float64
}
func planetHeliocentricXYZN(planetIndex int, jd float64, n int) (float64, float64, float64) {
l := planet.WherePlanetN(planetIndex, 0, jd, n)
b := planet.WherePlanetN(planetIndex, 1, jd, n)
r := planet.WherePlanetN(planetIndex, 2, jd, n)
return sphericalToRectangular(l, b, r)
}
func earthHeliocentricXYZN(jd float64, n int) (float64, float64, float64) {
l := planet.WherePlanetN(-1, 0, jd, n)
b := planet.WherePlanetN(-1, 1, jd, n)
r := planet.WherePlanetN(-1, 2, jd, n)
return sphericalToRectangular(l, b, r)
}
func sphericalToRectangular(lo, bo, radius float64) (float64, float64, float64) {
cosBo := math.Cos(bo * math.Pi / 180)
return radius * cosBo * math.Cos(lo*math.Pi/180),
radius * cosBo * math.Sin(lo*math.Pi/180),
radius * math.Sin(bo*math.Pi/180)
}
func geocentricPositionFromRectangular(x, y, z float64) planetGeocentricPosition {
lo := math.Atan2(y, x) * 180 / math.Pi
bo := math.Atan2(z, math.Sqrt(x*x+y*y)) * 180 / math.Pi
return planetGeocentricPosition{
x: x,
y: y,
z: z,
lo: Limit360(lo),
bo: bo,
}
}
func planetGeocentricPositionN(planetIndex int, planetJD, earthJD float64, n int) planetGeocentricPosition {
px, py, pz := planetHeliocentricXYZN(planetIndex, planetJD, n)
ex, ey, ez := earthHeliocentricXYZN(earthJD, n)
return geocentricPositionFromRectangular(px-ex, py-ey, pz-ez)
}
func planetGeocentricPositionWithEarthN(planetIndex int, planetJD float64, ex, ey, ez float64, n int) planetGeocentricPosition {
px, py, pz := planetHeliocentricXYZN(planetIndex, planetJD, n)
return geocentricPositionFromRectangular(px-ex, py-ey, pz-ez)
}
func planetApparentGeocentricPositionN(planetIndex int, jd float64, n int) (planetGeocentricPosition, float64) {
ex, ey, ez := earthHeliocentricXYZN(jd, n)
geoNow := planetGeocentricPositionWithEarthN(planetIndex, jd, ex, ey, ez, n)
tau := 0.0057755183 * math.Sqrt(geoNow.x*geoNow.x+geoNow.y*geoNow.y+geoNow.z*geoNow.z)
geo := planetGeocentricPositionWithEarthN(planetIndex, jd-tau, ex, ey, ez, n)
baseLo := geo.lo
baseBo := geo.bo
geo.lo = Limit360(baseLo + GXCLo(baseLo, baseBo, jd)/3600.0 + Nutation2000Bi(jd))
geo.bo = baseBo + GXCBo(baseLo, baseBo, jd)/3600.0
return geo, tau
}
func planetTrueGeocentricPositionN(planetIndex int, jd float64, n int) (planetGeocentricPosition, float64) {
ex, ey, ez := earthHeliocentricXYZN(jd, n)
geoNow := planetGeocentricPositionWithEarthN(planetIndex, jd, ex, ey, ez, n)
tau := 0.0057755183 * math.Sqrt(geoNow.x*geoNow.x+geoNow.y*geoNow.y+geoNow.z*geoNow.z)
return planetGeocentricPositionWithEarthN(planetIndex, jd-tau, ex, ey, ez, n), tau
}
func planetEarthAwayExplicitN(planetIndex int, jd float64, n int) float64 {
geoNow := planetGeocentricPositionN(planetIndex, jd, jd, n)
return math.Sqrt(geoNow.x*geoNow.x + geoNow.y*geoNow.y + geoNow.z*geoNow.z)
}
+83
View File
@@ -0,0 +1,83 @@
package basic
import (
"encoding/json"
"os"
"testing"
"time"
)
type planetApparentSample struct {
Body string `json:"body"`
InputUTC string `json:"input_utc"`
RightAscension float64 `json:"right_ascension"`
Declination float64 `json:"declination"`
EclipticLongitude float64 `json:"ecliptic_longitude"`
EclipticLatitude float64 `json:"ecliptic_latitude"`
}
func TestPlanetApparentCoordinatesMatchHorizonsBaseline(t *testing.T) {
data, err := os.ReadFile("testdata/planet_apparent_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []planetApparentSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
type apparentCase struct {
lo func(float64) float64
bo func(float64) float64
ra func(float64) float64
dec func(float64) float64
}
cases := map[string]apparentCase{
"mercury": {lo: MercuryApparentLo, bo: MercuryApparentBo, ra: MercuryApparentRa, dec: MercuryApparentDec},
"venus": {lo: VenusApparentLo, bo: VenusApparentBo, ra: VenusApparentRa, dec: VenusApparentDec},
"mars": {lo: MarsApparentLo, bo: MarsApparentBo, ra: MarsApparentRa, dec: MarsApparentDec},
"jupiter": {lo: JupiterApparentLo, bo: JupiterApparentBo, ra: JupiterApparentRa, dec: JupiterApparentDec},
"saturn": {lo: SaturnApparentLo, bo: SaturnApparentBo, ra: SaturnApparentRa, dec: SaturnApparentDec},
"uranus": {lo: UranusApparentLo, bo: UranusApparentBo, ra: UranusApparentRa, dec: UranusApparentDec},
"neptune": {lo: NeptuneApparentLo, bo: NeptuneApparentBo, ra: NeptuneApparentRa, dec: NeptuneApparentDec},
}
seen := make(map[string]bool, len(cases))
for _, sample := range samples {
tc, ok := cases[sample.Body]
if !ok {
t.Fatalf("unknown body %q", sample.Body)
}
if seen[sample.Body] {
t.Fatalf("duplicate body %q in apparent baseline", sample.Body)
}
seen[sample.Body] = true
date, err := time.Parse(time.RFC3339, sample.InputUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.InputUTC, err)
}
jd := TD2UT(Date2JDE(date.UTC()), true)
prefix := sample.Body + "." + sample.InputUTC
assertPlanetApparentAngleClose(t, prefix+".RightAscension", tc.ra(jd), sample.RightAscension, 0.001)
assertPlanetPhaseClose(t, prefix+".Declination", tc.dec(jd), sample.Declination, 0.001)
assertPlanetApparentAngleClose(t, prefix+".EclipticLongitude", tc.lo(jd), sample.EclipticLongitude, 0.001)
assertPlanetPhaseClose(t, prefix+".EclipticLatitude", tc.bo(jd), sample.EclipticLatitude, 0.001)
}
for body := range cases {
if !seen[body] {
t.Fatalf("missing body %q in apparent baseline", body)
}
}
}
func assertPlanetApparentAngleClose(t *testing.T, name string, got, want, tolerance float64) {
t.Helper()
if diff := angleDiffAbs(got, want); diff > tolerance {
t.Fatalf("%s mismatch: got %.12f want %.12f diff %.12f tolerance %.12f", name, got, want, diff, tolerance)
}
}
+200
View File
@@ -0,0 +1,200 @@
package basic_test
import (
"math"
"testing"
"time"
"b612.me/astro/basic"
)
func TestBasicPlanetObservationNFullMatchesDefault(t *testing.T) {
date := time.Date(2026, 4, 26, 9, 30, 45, 123456789, time.FixedZone("CST", 8*3600))
ttJD := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
jde := basic.Date2JDE(date)
lon := 116.391
lat := 39.907
tz := 8.0
height := 45.0
assertSame := func(name string, got, want float64) {
t.Helper()
if math.Float64bits(got) != math.Float64bits(want) {
t.Fatalf("%s full-n mismatch", name)
}
}
assertSamePair := func(name string, got1, got2, want1, want2 float64) {
t.Helper()
assertSame(name+".1", got1, want1)
assertSame(name+".2", got2, want2)
}
assertSameErr := func(name string, got, want error) {
t.Helper()
if got != want {
t.Fatalf("%s full-n mismatch", name)
}
}
floatChecks := []struct {
name string
got func() float64
want func() float64
}{
{"MercuryApparentLo", func() float64 { return basic.MercuryApparentLo(ttJD) }, func() float64 { return basic.MercuryApparentLoN(ttJD, -1) }},
{"MercuryApparentBo", func() float64 { return basic.MercuryApparentBo(ttJD) }, func() float64 { return basic.MercuryApparentBoN(ttJD, -1) }},
{"MercuryApparentRa", func() float64 { return basic.MercuryApparentRa(ttJD) }, func() float64 { return basic.MercuryApparentRaN(ttJD, -1) }},
{"MercuryApparentDec", func() float64 { return basic.MercuryApparentDec(ttJD) }, func() float64 { return basic.MercuryApparentDecN(ttJD, -1) }},
{"EarthMercuryAway", func() float64 { return basic.EarthMercuryAway(ttJD) }, func() float64 { return basic.EarthMercuryAwayN(ttJD, -1) }},
{"MercuryMag", func() float64 { return basic.MercuryMag(ttJD) }, func() float64 { return basic.MercuryMagN(ttJD, -1) }},
{"MercuryPhaseAngle", func() float64 { return basic.MercuryPhaseAngle(ttJD) }, func() float64 { return basic.MercuryPhaseAngleN(ttJD, -1) }},
{"MercuryIlluminatedFraction", func() float64 { return basic.MercuryIlluminatedFraction(ttJD) }, func() float64 { return basic.MercuryIlluminatedFractionN(ttJD, -1) }},
{"MercuryBrightLimbPositionAngle", func() float64 { return basic.MercuryBrightLimbPositionAngle(ttJD) }, func() float64 { return basic.MercuryBrightLimbPositionAngleN(ttJD, -1) }},
{"MercuryHeight", func() float64 { return basic.MercuryHeight(jde, lon, lat, tz) }, func() float64 { return basic.MercuryHeightN(jde, lon, lat, tz, -1) }},
{"MercuryAzimuth", func() float64 { return basic.MercuryAzimuth(jde, lon, lat, tz) }, func() float64 { return basic.MercuryAzimuthN(jde, lon, lat, tz, -1) }},
{"MercuryHourAngle", func() float64 { return basic.MercuryHourAngle(jde, lon, tz) }, func() float64 { return basic.MercuryHourAngleN(jde, lon, tz, -1) }},
{"MercuryCulminationTime", func() float64 { return basic.MercuryCulminationTime(jde, lon, tz) }, func() float64 { return basic.MercuryCulminationTimeN(jde, lon, tz, -1) }},
{"MercuryRiseTime", func() float64 { value, _ := basic.MercuryRiseTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.MercuryRiseTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"MercurySetTime", func() float64 { value, _ := basic.MercurySetTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.MercurySetTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"VenusApparentLo", func() float64 { return basic.VenusApparentLo(ttJD) }, func() float64 { return basic.VenusApparentLoN(ttJD, -1) }},
{"VenusApparentBo", func() float64 { return basic.VenusApparentBo(ttJD) }, func() float64 { return basic.VenusApparentBoN(ttJD, -1) }},
{"VenusApparentRa", func() float64 { return basic.VenusApparentRa(ttJD) }, func() float64 { return basic.VenusApparentRaN(ttJD, -1) }},
{"VenusApparentDec", func() float64 { return basic.VenusApparentDec(ttJD) }, func() float64 { return basic.VenusApparentDecN(ttJD, -1) }},
{"EarthVenusAway", func() float64 { return basic.EarthVenusAway(ttJD) }, func() float64 { return basic.EarthVenusAwayN(ttJD, -1) }},
{"VenusMag", func() float64 { return basic.VenusMag(ttJD) }, func() float64 { return basic.VenusMagN(ttJD, -1) }},
{"VenusPhaseAngle", func() float64 { return basic.VenusPhaseAngle(ttJD) }, func() float64 { return basic.VenusPhaseAngleN(ttJD, -1) }},
{"VenusIlluminatedFraction", func() float64 { return basic.VenusIlluminatedFraction(ttJD) }, func() float64 { return basic.VenusIlluminatedFractionN(ttJD, -1) }},
{"VenusBrightLimbPositionAngle", func() float64 { return basic.VenusBrightLimbPositionAngle(ttJD) }, func() float64 { return basic.VenusBrightLimbPositionAngleN(ttJD, -1) }},
{"VenusHeight", func() float64 { return basic.VenusHeight(jde, lon, lat, tz) }, func() float64 { return basic.VenusHeightN(jde, lon, lat, tz, -1) }},
{"VenusAzimuth", func() float64 { return basic.VenusAzimuth(jde, lon, lat, tz) }, func() float64 { return basic.VenusAzimuthN(jde, lon, lat, tz, -1) }},
{"VenusHourAngle", func() float64 { return basic.VenusHourAngle(jde, lon, tz) }, func() float64 { return basic.VenusHourAngleN(jde, lon, tz, -1) }},
{"VenusCulminationTime", func() float64 { return basic.VenusCulminationTime(jde, lon, tz) }, func() float64 { return basic.VenusCulminationTimeN(jde, lon, tz, -1) }},
{"VenusRiseTime", func() float64 { value, _ := basic.VenusRiseTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.VenusRiseTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"VenusSetTime", func() float64 { value, _ := basic.VenusSetTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.VenusSetTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"MarsApparentLo", func() float64 { return basic.MarsApparentLo(ttJD) }, func() float64 { return basic.MarsApparentLoN(ttJD, -1) }},
{"MarsApparentBo", func() float64 { return basic.MarsApparentBo(ttJD) }, func() float64 { return basic.MarsApparentBoN(ttJD, -1) }},
{"MarsApparentRa", func() float64 { return basic.MarsApparentRa(ttJD) }, func() float64 { return basic.MarsApparentRaN(ttJD, -1) }},
{"MarsApparentDec", func() float64 { return basic.MarsApparentDec(ttJD) }, func() float64 { return basic.MarsApparentDecN(ttJD, -1) }},
{"EarthMarsAway", func() float64 { return basic.EarthMarsAway(ttJD) }, func() float64 { return basic.EarthMarsAwayN(ttJD, -1) }},
{"MarsMag", func() float64 { return basic.MarsMag(ttJD) }, func() float64 { return basic.MarsMagN(ttJD, -1) }},
{"MarsPhaseAngle", func() float64 { return basic.MarsPhaseAngle(ttJD) }, func() float64 { return basic.MarsPhaseAngleN(ttJD, -1) }},
{"MarsIlluminatedFraction", func() float64 { return basic.MarsIlluminatedFraction(ttJD) }, func() float64 { return basic.MarsIlluminatedFractionN(ttJD, -1) }},
{"MarsBrightLimbPositionAngle", func() float64 { return basic.MarsBrightLimbPositionAngle(ttJD) }, func() float64 { return basic.MarsBrightLimbPositionAngleN(ttJD, -1) }},
{"MarsHeight", func() float64 { return basic.MarsHeight(jde, lon, lat, tz) }, func() float64 { return basic.MarsHeightN(jde, lon, lat, tz, -1) }},
{"MarsAzimuth", func() float64 { return basic.MarsAzimuth(jde, lon, lat, tz) }, func() float64 { return basic.MarsAzimuthN(jde, lon, lat, tz, -1) }},
{"MarsHourAngle", func() float64 { return basic.MarsHourAngle(jde, lon, tz) }, func() float64 { return basic.MarsHourAngleN(jde, lon, tz, -1) }},
{"MarsCulminationTime", func() float64 { return basic.MarsCulminationTime(jde, lon, tz) }, func() float64 { return basic.MarsCulminationTimeN(jde, lon, tz, -1) }},
{"MarsRiseTime", func() float64 { value, _ := basic.MarsRiseTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.MarsRiseTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"MarsSetTime", func() float64 { value, _ := basic.MarsSetTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.MarsSetTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"JupiterApparentLo", func() float64 { return basic.JupiterApparentLo(ttJD) }, func() float64 { return basic.JupiterApparentLoN(ttJD, -1) }},
{"JupiterApparentBo", func() float64 { return basic.JupiterApparentBo(ttJD) }, func() float64 { return basic.JupiterApparentBoN(ttJD, -1) }},
{"JupiterApparentRa", func() float64 { return basic.JupiterApparentRa(ttJD) }, func() float64 { return basic.JupiterApparentRaN(ttJD, -1) }},
{"JupiterApparentDec", func() float64 { return basic.JupiterApparentDec(ttJD) }, func() float64 { return basic.JupiterApparentDecN(ttJD, -1) }},
{"EarthJupiterAway", func() float64 { return basic.EarthJupiterAway(ttJD) }, func() float64 { return basic.EarthJupiterAwayN(ttJD, -1) }},
{"JupiterMag", func() float64 { return basic.JupiterMag(ttJD) }, func() float64 { return basic.JupiterMagN(ttJD, -1) }},
{"JupiterPhaseAngle", func() float64 { return basic.JupiterPhaseAngle(ttJD) }, func() float64 { return basic.JupiterPhaseAngleN(ttJD, -1) }},
{"JupiterIlluminatedFraction", func() float64 { return basic.JupiterIlluminatedFraction(ttJD) }, func() float64 { return basic.JupiterIlluminatedFractionN(ttJD, -1) }},
{"JupiterBrightLimbPositionAngle", func() float64 { return basic.JupiterBrightLimbPositionAngle(ttJD) }, func() float64 { return basic.JupiterBrightLimbPositionAngleN(ttJD, -1) }},
{"JupiterHeight", func() float64 { return basic.JupiterHeight(jde, lon, lat, tz) }, func() float64 { return basic.JupiterHeightN(jde, lon, lat, tz, -1) }},
{"JupiterAzimuth", func() float64 { return basic.JupiterAzimuth(jde, lon, lat, tz) }, func() float64 { return basic.JupiterAzimuthN(jde, lon, lat, tz, -1) }},
{"JupiterHourAngle", func() float64 { return basic.JupiterHourAngle(jde, lon, tz) }, func() float64 { return basic.JupiterHourAngleN(jde, lon, tz, -1) }},
{"JupiterCulminationTime", func() float64 { return basic.JupiterCulminationTime(jde, lon, tz) }, func() float64 { return basic.JupiterCulminationTimeN(jde, lon, tz, -1) }},
{"JupiterRiseTime", func() float64 { value, _ := basic.JupiterRiseTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.JupiterRiseTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"JupiterSetTime", func() float64 { value, _ := basic.JupiterSetTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.JupiterSetTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"SaturnApparentLo", func() float64 { return basic.SaturnApparentLo(ttJD) }, func() float64 { return basic.SaturnApparentLoN(ttJD, -1) }},
{"SaturnApparentBo", func() float64 { return basic.SaturnApparentBo(ttJD) }, func() float64 { return basic.SaturnApparentBoN(ttJD, -1) }},
{"SaturnApparentRa", func() float64 { return basic.SaturnApparentRa(ttJD) }, func() float64 { return basic.SaturnApparentRaN(ttJD, -1) }},
{"SaturnApparentDec", func() float64 { return basic.SaturnApparentDec(ttJD) }, func() float64 { return basic.SaturnApparentDecN(ttJD, -1) }},
{"EarthSaturnAway", func() float64 { return basic.EarthSaturnAway(ttJD) }, func() float64 { return basic.EarthSaturnAwayN(ttJD, -1) }},
{"SaturnRingB", func() float64 { return basic.SaturnRingB(ttJD) }, func() float64 { return basic.SaturnRingBN(ttJD, -1) }},
{"SaturnRingSunB", func() float64 { return basic.SaturnRingSunB(ttJD) }, func() float64 { return basic.SaturnRingSunBN(ttJD, -1) }},
{"SaturnRingPositionAngle", func() float64 { return basic.SaturnRingPositionAngle(ttJD) }, func() float64 { return basic.SaturnRingPositionAngleN(ttJD, -1) }},
{"SaturnRingDeltaU", func() float64 { return basic.SaturnRingDeltaU(ttJD) }, func() float64 { return basic.SaturnRingDeltaUN(ttJD, -1) }},
{"SaturnMag", func() float64 { return basic.SaturnMag(ttJD) }, func() float64 { return basic.SaturnMagN(ttJD, -1) }},
{"SaturnPhaseAngle", func() float64 { return basic.SaturnPhaseAngle(ttJD) }, func() float64 { return basic.SaturnPhaseAngleN(ttJD, -1) }},
{"SaturnIlluminatedFraction", func() float64 { return basic.SaturnIlluminatedFraction(ttJD) }, func() float64 { return basic.SaturnIlluminatedFractionN(ttJD, -1) }},
{"SaturnBrightLimbPositionAngle", func() float64 { return basic.SaturnBrightLimbPositionAngle(ttJD) }, func() float64 { return basic.SaturnBrightLimbPositionAngleN(ttJD, -1) }},
{"SaturnHeight", func() float64 { return basic.SaturnHeight(jde, lon, lat, tz) }, func() float64 { return basic.SaturnHeightN(jde, lon, lat, tz, -1) }},
{"SaturnAzimuth", func() float64 { return basic.SaturnAzimuth(jde, lon, lat, tz) }, func() float64 { return basic.SaturnAzimuthN(jde, lon, lat, tz, -1) }},
{"SaturnHourAngle", func() float64 { return basic.SaturnHourAngle(jde, lon, tz) }, func() float64 { return basic.SaturnHourAngleN(jde, lon, tz, -1) }},
{"SaturnCulminationTime", func() float64 { return basic.SaturnCulminationTime(jde, lon, tz) }, func() float64 { return basic.SaturnCulminationTimeN(jde, lon, tz, -1) }},
{"SaturnRiseTime", func() float64 { value, _ := basic.SaturnRiseTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.SaturnRiseTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"SaturnSetTime", func() float64 { value, _ := basic.SaturnSetTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.SaturnSetTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"UranusApparentLo", func() float64 { return basic.UranusApparentLo(ttJD) }, func() float64 { return basic.UranusApparentLoN(ttJD, -1) }},
{"UranusApparentBo", func() float64 { return basic.UranusApparentBo(ttJD) }, func() float64 { return basic.UranusApparentBoN(ttJD, -1) }},
{"UranusApparentRa", func() float64 { return basic.UranusApparentRa(ttJD) }, func() float64 { return basic.UranusApparentRaN(ttJD, -1) }},
{"UranusApparentDec", func() float64 { return basic.UranusApparentDec(ttJD) }, func() float64 { return basic.UranusApparentDecN(ttJD, -1) }},
{"EarthUranusAway", func() float64 { return basic.EarthUranusAway(ttJD) }, func() float64 { return basic.EarthUranusAwayN(ttJD, -1) }},
{"UranusMag", func() float64 { return basic.UranusMag(ttJD) }, func() float64 { return basic.UranusMagN(ttJD, -1) }},
{"UranusPhaseAngle", func() float64 { return basic.UranusPhaseAngle(ttJD) }, func() float64 { return basic.UranusPhaseAngleN(ttJD, -1) }},
{"UranusIlluminatedFraction", func() float64 { return basic.UranusIlluminatedFraction(ttJD) }, func() float64 { return basic.UranusIlluminatedFractionN(ttJD, -1) }},
{"UranusBrightLimbPositionAngle", func() float64 { return basic.UranusBrightLimbPositionAngle(ttJD) }, func() float64 { return basic.UranusBrightLimbPositionAngleN(ttJD, -1) }},
{"UranusHeight", func() float64 { return basic.UranusHeight(jde, lon, lat, tz) }, func() float64 { return basic.UranusHeightN(jde, lon, lat, tz, -1) }},
{"UranusAzimuth", func() float64 { return basic.UranusAzimuth(jde, lon, lat, tz) }, func() float64 { return basic.UranusAzimuthN(jde, lon, lat, tz, -1) }},
{"UranusHourAngle", func() float64 { return basic.UranusHourAngle(jde, lon, tz) }, func() float64 { return basic.UranusHourAngleN(jde, lon, tz, -1) }},
{"UranusCulminationTime", func() float64 { return basic.UranusCulminationTime(jde, lon, tz) }, func() float64 { return basic.UranusCulminationTimeN(jde, lon, tz, -1) }},
{"UranusRiseTime", func() float64 { value, _ := basic.UranusRiseTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.UranusRiseTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"UranusSetTime", func() float64 { value, _ := basic.UranusSetTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.UranusSetTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"NeptuneApparentLo", func() float64 { return basic.NeptuneApparentLo(ttJD) }, func() float64 { return basic.NeptuneApparentLoN(ttJD, -1) }},
{"NeptuneApparentBo", func() float64 { return basic.NeptuneApparentBo(ttJD) }, func() float64 { return basic.NeptuneApparentBoN(ttJD, -1) }},
{"NeptuneApparentRa", func() float64 { return basic.NeptuneApparentRa(ttJD) }, func() float64 { return basic.NeptuneApparentRaN(ttJD, -1) }},
{"NeptuneApparentDec", func() float64 { return basic.NeptuneApparentDec(ttJD) }, func() float64 { return basic.NeptuneApparentDecN(ttJD, -1) }},
{"EarthNeptuneAway", func() float64 { return basic.EarthNeptuneAway(ttJD) }, func() float64 { return basic.EarthNeptuneAwayN(ttJD, -1) }},
{"NeptuneMag", func() float64 { return basic.NeptuneMag(ttJD) }, func() float64 { return basic.NeptuneMagN(ttJD, -1) }},
{"NeptunePhaseAngle", func() float64 { return basic.NeptunePhaseAngle(ttJD) }, func() float64 { return basic.NeptunePhaseAngleN(ttJD, -1) }},
{"NeptuneIlluminatedFraction", func() float64 { return basic.NeptuneIlluminatedFraction(ttJD) }, func() float64 { return basic.NeptuneIlluminatedFractionN(ttJD, -1) }},
{"NeptuneBrightLimbPositionAngle", func() float64 { return basic.NeptuneBrightLimbPositionAngle(ttJD) }, func() float64 { return basic.NeptuneBrightLimbPositionAngleN(ttJD, -1) }},
{"NeptuneHeight", func() float64 { return basic.NeptuneHeight(jde, lon, lat, tz) }, func() float64 { return basic.NeptuneHeightN(jde, lon, lat, tz, -1) }},
{"NeptuneAzimuth", func() float64 { return basic.NeptuneAzimuth(jde, lon, lat, tz) }, func() float64 { return basic.NeptuneAzimuthN(jde, lon, lat, tz, -1) }},
{"NeptuneHourAngle", func() float64 { return basic.NeptuneHourAngle(jde, lon, tz) }, func() float64 { return basic.NeptuneHourAngleN(jde, lon, tz, -1) }},
{"NeptuneCulminationTime", func() float64 { return basic.NeptuneCulminationTime(jde, lon, tz) }, func() float64 { return basic.NeptuneCulminationTimeN(jde, lon, tz, -1) }},
{"NeptuneRiseTime", func() float64 { value, _ := basic.NeptuneRiseTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.NeptuneRiseTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
{"NeptuneSetTime", func() float64 { value, _ := basic.NeptuneSetTime(jde, lon, lat, tz, 1, height); return value }, func() float64 { value, _ := basic.NeptuneSetTimeN(jde, lon, lat, tz, 1, height, -1); return value }},
}
for _, tc := range floatChecks {
assertSame(tc.name, tc.got(), tc.want())
}
errorChecks := []struct {
name string
got func() error
want func() error
}{
{"MercuryRiseTime.err", func() error { _, err := basic.MercuryRiseTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.MercuryRiseTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"MercurySetTime.err", func() error { _, err := basic.MercurySetTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.MercurySetTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"VenusRiseTime.err", func() error { _, err := basic.VenusRiseTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.VenusRiseTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"VenusSetTime.err", func() error { _, err := basic.VenusSetTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.VenusSetTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"MarsRiseTime.err", func() error { _, err := basic.MarsRiseTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.MarsRiseTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"MarsSetTime.err", func() error { _, err := basic.MarsSetTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.MarsSetTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"JupiterRiseTime.err", func() error { _, err := basic.JupiterRiseTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.JupiterRiseTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"JupiterSetTime.err", func() error { _, err := basic.JupiterSetTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.JupiterSetTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"SaturnRiseTime.err", func() error { _, err := basic.SaturnRiseTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.SaturnRiseTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"SaturnSetTime.err", func() error { _, err := basic.SaturnSetTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.SaturnSetTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"UranusRiseTime.err", func() error { _, err := basic.UranusRiseTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.UranusRiseTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"UranusSetTime.err", func() error { _, err := basic.UranusSetTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.UranusSetTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"NeptuneRiseTime.err", func() error { _, err := basic.NeptuneRiseTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.NeptuneRiseTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
{"NeptuneSetTime.err", func() error { _, err := basic.NeptuneSetTime(jde, lon, lat, tz, 1, height); return err }, func() error { _, err := basic.NeptuneSetTimeN(jde, lon, lat, tz, 1, height, -1); return err }},
}
for _, tc := range errorChecks {
assertSameErr(tc.name, tc.got(), tc.want())
}
pairChecks := []struct {
name string
got func() (float64, float64)
want func() (float64, float64)
}{
{"MercuryApparentRaDec", func() (float64, float64) { return basic.MercuryApparentRaDec(ttJD) }, func() (float64, float64) { return basic.MercuryApparentRaDecN(ttJD, -1) }},
{"VenusApparentRaDec", func() (float64, float64) { return basic.VenusApparentRaDec(ttJD) }, func() (float64, float64) { return basic.VenusApparentRaDecN(ttJD, -1) }},
{"MarsApparentRaDec", func() (float64, float64) { return basic.MarsApparentRaDec(ttJD) }, func() (float64, float64) { return basic.MarsApparentRaDecN(ttJD, -1) }},
{"JupiterApparentRaDec", func() (float64, float64) { return basic.JupiterApparentRaDec(ttJD) }, func() (float64, float64) { return basic.JupiterApparentRaDecN(ttJD, -1) }},
{"SaturnApparentRaDec", func() (float64, float64) { return basic.SaturnApparentRaDec(ttJD) }, func() (float64, float64) { return basic.SaturnApparentRaDecN(ttJD, -1) }},
{"SaturnRingAxis", func() (float64, float64) { return basic.SaturnRingAxis(ttJD) }, func() (float64, float64) { return basic.SaturnRingAxisN(ttJD, -1) }},
{"UranusApparentRaDec", func() (float64, float64) { return basic.UranusApparentRaDec(ttJD) }, func() (float64, float64) { return basic.UranusApparentRaDecN(ttJD, -1) }},
{"NeptuneApparentRaDec", func() (float64, float64) { return basic.NeptuneApparentRaDec(ttJD) }, func() (float64, float64) { return basic.NeptuneApparentRaDecN(ttJD, -1) }},
}
for _, tc := range pairChecks {
got1, got2 := tc.got()
want1, want2 := tc.want()
assertSamePair(tc.name, got1, got2, want1, want2)
}
}
+252
View File
@@ -0,0 +1,252 @@
package basic
import (
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
type planetRaDecNFunc func(jd float64, n int) (float64, float64)
// MercuryPhaseAngle 水星相位角 / phase angle of Mercury.
func MercuryPhaseAngle(jd float64) float64 {
return MercuryPhaseAngleN(jd, -1)
}
// MercuryPhaseAngleN 水星相位角(截断版) / truncated phase angle of Mercury.
func MercuryPhaseAngleN(jd float64, n int) float64 {
return planetPhaseAngleN(1, jd, n)
}
// MercuryIlluminatedFraction 水星被照亮比例 / illuminated fraction of Mercury.
func MercuryIlluminatedFraction(jd float64) float64 {
return MercuryIlluminatedFractionN(jd, -1)
}
// MercuryIlluminatedFractionN 水星被照亮比例(截断版) / truncated illuminated fraction of Mercury.
func MercuryIlluminatedFractionN(jd float64, n int) float64 {
return planetIlluminatedFractionN(1, jd, n)
}
// MercuryBrightLimbPositionAngle 水星亮面中心位置角 / position angle of Mercury bright limb.
func MercuryBrightLimbPositionAngle(jd float64) float64 {
return MercuryBrightLimbPositionAngleN(jd, -1)
}
// MercuryBrightLimbPositionAngleN 水星亮面中心位置角(截断版) / truncated position angle of Mercury bright limb.
func MercuryBrightLimbPositionAngleN(jd float64, n int) float64 {
return planetBrightLimbPositionAngleN(jd, n, MercuryApparentRaDecN)
}
// VenusPhaseAngle 金星相位角 / phase angle of Venus.
func VenusPhaseAngle(jd float64) float64 {
return VenusPhaseAngleN(jd, -1)
}
// VenusPhaseAngleN 金星相位角(截断版) / truncated phase angle of Venus.
func VenusPhaseAngleN(jd float64, n int) float64 {
return planetPhaseAngleN(2, jd, n)
}
// VenusIlluminatedFraction 金星被照亮比例 / illuminated fraction of Venus.
func VenusIlluminatedFraction(jd float64) float64 {
return VenusIlluminatedFractionN(jd, -1)
}
// VenusIlluminatedFractionN 金星被照亮比例(截断版) / truncated illuminated fraction of Venus.
func VenusIlluminatedFractionN(jd float64, n int) float64 {
return planetIlluminatedFractionN(2, jd, n)
}
// VenusBrightLimbPositionAngle 金星亮面中心位置角 / position angle of Venus bright limb.
func VenusBrightLimbPositionAngle(jd float64) float64 {
return VenusBrightLimbPositionAngleN(jd, -1)
}
// VenusBrightLimbPositionAngleN 金星亮面中心位置角(截断版) / truncated position angle of Venus bright limb.
func VenusBrightLimbPositionAngleN(jd float64, n int) float64 {
return planetBrightLimbPositionAngleN(jd, n, VenusApparentRaDecN)
}
// MarsPhaseAngle 火星相位角 / phase angle of Mars.
func MarsPhaseAngle(jd float64) float64 {
return MarsPhaseAngleN(jd, -1)
}
// MarsPhaseAngleN 火星相位角(截断版) / truncated phase angle of Mars.
func MarsPhaseAngleN(jd float64, n int) float64 {
return planetPhaseAngleN(3, jd, n)
}
// MarsIlluminatedFraction 火星被照亮比例 / illuminated fraction of Mars.
func MarsIlluminatedFraction(jd float64) float64 {
return MarsIlluminatedFractionN(jd, -1)
}
// MarsIlluminatedFractionN 火星被照亮比例(截断版) / truncated illuminated fraction of Mars.
func MarsIlluminatedFractionN(jd float64, n int) float64 {
return planetIlluminatedFractionN(3, jd, n)
}
// MarsBrightLimbPositionAngle 火星亮面中心位置角 / position angle of Mars bright limb.
func MarsBrightLimbPositionAngle(jd float64) float64 {
return MarsBrightLimbPositionAngleN(jd, -1)
}
// MarsBrightLimbPositionAngleN 火星亮面中心位置角(截断版) / truncated position angle of Mars bright limb.
func MarsBrightLimbPositionAngleN(jd float64, n int) float64 {
return planetBrightLimbPositionAngleN(jd, n, MarsApparentRaDecN)
}
// JupiterPhaseAngle 木星相位角 / phase angle of Jupiter.
func JupiterPhaseAngle(jd float64) float64 {
return JupiterPhaseAngleN(jd, -1)
}
// JupiterPhaseAngleN 木星相位角(截断版) / truncated phase angle of Jupiter.
func JupiterPhaseAngleN(jd float64, n int) float64 {
return planetPhaseAngleN(4, jd, n)
}
// JupiterIlluminatedFraction 木星被照亮比例 / illuminated fraction of Jupiter.
func JupiterIlluminatedFraction(jd float64) float64 {
return JupiterIlluminatedFractionN(jd, -1)
}
// JupiterIlluminatedFractionN 木星被照亮比例(截断版) / truncated illuminated fraction of Jupiter.
func JupiterIlluminatedFractionN(jd float64, n int) float64 {
return planetIlluminatedFractionN(4, jd, n)
}
// JupiterBrightLimbPositionAngle 木星亮面中心位置角 / position angle of Jupiter bright limb.
func JupiterBrightLimbPositionAngle(jd float64) float64 {
return JupiterBrightLimbPositionAngleN(jd, -1)
}
// JupiterBrightLimbPositionAngleN 木星亮面中心位置角(截断版) / truncated position angle of Jupiter bright limb.
func JupiterBrightLimbPositionAngleN(jd float64, n int) float64 {
return planetBrightLimbPositionAngleN(jd, n, JupiterApparentRaDecN)
}
// SaturnPhaseAngle 土星相位角 / phase angle of Saturn.
func SaturnPhaseAngle(jd float64) float64 {
return SaturnPhaseAngleN(jd, -1)
}
// SaturnPhaseAngleN 土星相位角(截断版) / truncated phase angle of Saturn.
func SaturnPhaseAngleN(jd float64, n int) float64 {
return planetPhaseAngleN(5, jd, n)
}
// SaturnIlluminatedFraction 土星被照亮比例 / illuminated fraction of Saturn.
func SaturnIlluminatedFraction(jd float64) float64 {
return SaturnIlluminatedFractionN(jd, -1)
}
// SaturnIlluminatedFractionN 土星被照亮比例(截断版) / truncated illuminated fraction of Saturn.
func SaturnIlluminatedFractionN(jd float64, n int) float64 {
return planetIlluminatedFractionN(5, jd, n)
}
// SaturnBrightLimbPositionAngle 土星亮面中心位置角 / position angle of Saturn bright limb.
func SaturnBrightLimbPositionAngle(jd float64) float64 {
return SaturnBrightLimbPositionAngleN(jd, -1)
}
// SaturnBrightLimbPositionAngleN 土星亮面中心位置角(截断版) / truncated position angle of Saturn bright limb.
func SaturnBrightLimbPositionAngleN(jd float64, n int) float64 {
return planetBrightLimbPositionAngleN(jd, n, SaturnApparentRaDecN)
}
// UranusPhaseAngle 天王星相位角 / phase angle of Uranus.
func UranusPhaseAngle(jd float64) float64 {
return UranusPhaseAngleN(jd, -1)
}
// UranusPhaseAngleN 天王星相位角(截断版) / truncated phase angle of Uranus.
func UranusPhaseAngleN(jd float64, n int) float64 {
return planetPhaseAngleN(6, jd, n)
}
// UranusIlluminatedFraction 天王星被照亮比例 / illuminated fraction of Uranus.
func UranusIlluminatedFraction(jd float64) float64 {
return UranusIlluminatedFractionN(jd, -1)
}
// UranusIlluminatedFractionN 天王星被照亮比例(截断版) / truncated illuminated fraction of Uranus.
func UranusIlluminatedFractionN(jd float64, n int) float64 {
return planetIlluminatedFractionN(6, jd, n)
}
// UranusBrightLimbPositionAngle 天王星亮面中心位置角 / position angle of Uranus bright limb.
func UranusBrightLimbPositionAngle(jd float64) float64 {
return UranusBrightLimbPositionAngleN(jd, -1)
}
// UranusBrightLimbPositionAngleN 天王星亮面中心位置角(截断版) / truncated position angle of Uranus bright limb.
func UranusBrightLimbPositionAngleN(jd float64, n int) float64 {
return planetBrightLimbPositionAngleN(jd, n, UranusApparentRaDecN)
}
// NeptunePhaseAngle 海王星相位角 / phase angle of Neptune.
func NeptunePhaseAngle(jd float64) float64 {
return NeptunePhaseAngleN(jd, -1)
}
// NeptunePhaseAngleN 海王星相位角(截断版) / truncated phase angle of Neptune.
func NeptunePhaseAngleN(jd float64, n int) float64 {
return planetPhaseAngleN(7, jd, n)
}
// NeptuneIlluminatedFraction 海王星被照亮比例 / illuminated fraction of Neptune.
func NeptuneIlluminatedFraction(jd float64) float64 {
return NeptuneIlluminatedFractionN(jd, -1)
}
// NeptuneIlluminatedFractionN 海王星被照亮比例(截断版) / truncated illuminated fraction of Neptune.
func NeptuneIlluminatedFractionN(jd float64, n int) float64 {
return planetIlluminatedFractionN(7, jd, n)
}
// NeptuneBrightLimbPositionAngle 海王星亮面中心位置角 / position angle of Neptune bright limb.
func NeptuneBrightLimbPositionAngle(jd float64) float64 {
return NeptuneBrightLimbPositionAngleN(jd, -1)
}
// NeptuneBrightLimbPositionAngleN 海王星亮面中心位置角(截断版) / truncated position angle of Neptune bright limb.
func NeptuneBrightLimbPositionAngleN(jd float64, n int) float64 {
return planetBrightLimbPositionAngleN(jd, n, NeptuneApparentRaDecN)
}
func planetPhaseAngleN(planetIndex int, jd float64, n int) float64 {
return ArcCos(planetPhaseCosineN(planetIndex, jd, n))
}
func planetIlluminatedFractionN(planetIndex int, jd float64, n int) float64 {
return (1 + planetPhaseCosineN(planetIndex, jd, n)) / 2
}
func planetPhaseCosineN(planetIndex int, jd float64, n int) float64 {
planetSunDistance := planet.WherePlanetN(planetIndex, 2, jd, n)
planetEarthDistance := planetEarthAwayN(planetIndex, jd, n)
earthSunDistance := EarthAwayN(jd, n)
cosine := (planetSunDistance*planetSunDistance + planetEarthDistance*planetEarthDistance - earthSunDistance*earthSunDistance) / (2 * planetSunDistance * planetEarthDistance)
return clampUnit(cosine)
}
func planetBrightLimbPositionAngleN(jd float64, n int, apparentRaDec planetRaDecNFunc) float64 {
sunRa, sunDec := HSunApparentRaDecN(jd, n)
planetRa, planetDec := apparentRaDec(jd, n)
y := Cos(sunDec) * Sin(sunRa-planetRa)
x := Sin(sunDec)*Cos(planetDec) - Cos(sunDec)*Sin(planetDec)*Cos(sunRa-planetRa)
return ArcTan2(y, x)
}
func clampUnit(value float64) float64 {
if value > 1 {
return 1
}
if value < -1 {
return -1
}
return value
}
+54
View File
@@ -0,0 +1,54 @@
package basic
import (
"math"
"testing"
"time"
)
func TestVenusIlluminatedFractionMeeusExample(t *testing.T) {
jd := Date2JDE(time.Date(1992, 12, 20, 0, 0, 0, 0, time.UTC))
assertPlanetPhaseClose(t, "VenusPhaseAngle", VenusPhaseAngle(jd), 72.96, 0.01)
assertPlanetPhaseClose(t, "VenusIlluminatedFraction", VenusIlluminatedFraction(jd), 0.647, 0.001)
}
func TestPlanetIlluminatedFractionRanges(t *testing.T) {
jd := TD2UT(Date2JDE(time.Date(2026, 4, 26, 9, 30, 45, 0, time.UTC)), true)
cases := []struct {
name string
phaseAngle func(float64) float64
fraction func(float64) float64
positionAngle func(float64) float64
}{
{"Mercury", MercuryPhaseAngle, MercuryIlluminatedFraction, MercuryBrightLimbPositionAngle},
{"Venus", VenusPhaseAngle, VenusIlluminatedFraction, VenusBrightLimbPositionAngle},
{"Mars", MarsPhaseAngle, MarsIlluminatedFraction, MarsBrightLimbPositionAngle},
{"Jupiter", JupiterPhaseAngle, JupiterIlluminatedFraction, JupiterBrightLimbPositionAngle},
{"Saturn", SaturnPhaseAngle, SaturnIlluminatedFraction, SaturnBrightLimbPositionAngle},
{"Uranus", UranusPhaseAngle, UranusIlluminatedFraction, UranusBrightLimbPositionAngle},
{"Neptune", NeptunePhaseAngle, NeptuneIlluminatedFraction, NeptuneBrightLimbPositionAngle},
}
for _, tc := range cases {
phaseAngle := tc.phaseAngle(jd)
if phaseAngle < 0 || phaseAngle > 180 {
t.Fatalf("%s phase angle out of range: %.12f", tc.name, phaseAngle)
}
fraction := tc.fraction(jd)
if fraction < 0 || fraction > 1 {
t.Fatalf("%s illuminated fraction out of range: %.12f", tc.name, fraction)
}
positionAngle := tc.positionAngle(jd)
if positionAngle < 0 || positionAngle >= 360 {
t.Fatalf("%s bright limb position angle out of range: %.12f", tc.name, positionAngle)
}
}
}
func assertPlanetPhaseClose(t *testing.T, name string, got, want, tolerance float64) {
t.Helper()
if math.Abs(got-want) > tolerance {
t.Fatalf("%s mismatch: got %.12f want %.12f tolerance %.12f", name, got, want, tolerance)
}
}
+370
View File
@@ -0,0 +1,370 @@
package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
const astronomicalUnitLightTimeDays = 0.0057755183
type planetPhysicalModel struct {
planetIndex int
positiveEast bool
equatorialRadius float64
polarRadius float64
poleRotation func(float64) (ra, dec, rotationEast float64)
}
type phaseAngleModel struct {
base float64
rate float64
accelRate float64
}
// PlanetPhysicalInfo 行星物理观测参数 / planetary physical observing parameters.
type PlanetPhysicalInfo struct {
// SubEarthLongitude 子地经度,单位度;正方向遵循该天体当前 IAU/Horizons 制图约定。
SubEarthLongitude float64
// SubEarthLatitude 子地纬度,单位度。
SubEarthLatitude float64
// SubSolarLongitude 子日经度,单位度;正方向遵循该天体当前 IAU/Horizons 制图约定。
SubSolarLongitude float64
// SubSolarLatitude 子日纬度,单位度。
SubSolarLatitude float64
// NorthPolePositionAngle 天体北极位置角,单位度。
NorthPolePositionAngle float64
}
var (
mercuryPhysicalModel = planetPhysicalModel{
planetIndex: 1,
positiveEast: false,
equatorialRadius: 2440.53,
polarRadius: 2438.26,
poleRotation: mercuryPoleRotation,
}
venusPhysicalModel = planetPhysicalModel{
planetIndex: 2,
positiveEast: true,
equatorialRadius: 6051.8,
polarRadius: 6051.8,
poleRotation: venusPoleRotation,
}
marsPhysicalModel = planetPhysicalModel{
planetIndex: 3,
positiveEast: false,
equatorialRadius: 3396.19,
polarRadius: 3376.20,
poleRotation: marsPoleRotation,
}
jupiterPhysicalModel = planetPhysicalModel{
planetIndex: 4,
positiveEast: false,
equatorialRadius: 71492.0,
polarRadius: 66854.0,
poleRotation: jupiterPoleRotation,
}
saturnPhysicalModel = planetPhysicalModel{
planetIndex: 5,
positiveEast: false,
equatorialRadius: 60268.0,
polarRadius: 54364.0,
poleRotation: saturnPoleRotation,
}
uranusPhysicalModel = planetPhysicalModel{
planetIndex: 6,
positiveEast: true,
equatorialRadius: 25559.0,
polarRadius: 24973.0,
poleRotation: uranusPoleRotation,
}
neptunePhysicalModel = planetPhysicalModel{
planetIndex: 7,
positiveEast: false,
equatorialRadius: 24764.0,
polarRadius: 24341.0,
poleRotation: neptunePoleRotation,
}
)
// MercuryPhysical 水星物理观测参数 / physical observing parameters of Mercury.
func MercuryPhysical(jd float64) PlanetPhysicalInfo {
return MercuryPhysicalN(jd, -1)
}
// MercuryPhysicalN 水星物理观测参数(截断版) / truncated physical observing parameters of Mercury.
func MercuryPhysicalN(jd float64, n int) PlanetPhysicalInfo {
return planetPhysicalN(jd, n, mercuryPhysicalModel)
}
// VenusPhysical 金星物理观测参数 / physical observing parameters of Venus.
func VenusPhysical(jd float64) PlanetPhysicalInfo {
return VenusPhysicalN(jd, -1)
}
// VenusPhysicalN 金星物理观测参数(截断版) / truncated physical observing parameters of Venus.
func VenusPhysicalN(jd float64, n int) PlanetPhysicalInfo {
return planetPhysicalN(jd, n, venusPhysicalModel)
}
// MarsPhysical 火星物理观测参数 / physical observing parameters of Mars.
func MarsPhysical(jd float64) PlanetPhysicalInfo {
return MarsPhysicalN(jd, -1)
}
// MarsPhysicalN 火星物理观测参数(截断版) / truncated physical observing parameters of Mars.
func MarsPhysicalN(jd float64, n int) PlanetPhysicalInfo {
return planetPhysicalN(jd, n, marsPhysicalModel)
}
// JupiterPhysical 木星物理观测参数 / physical observing parameters of Jupiter.
func JupiterPhysical(jd float64) PlanetPhysicalInfo {
return JupiterPhysicalN(jd, -1)
}
// JupiterPhysicalN 木星物理观测参数(截断版) / truncated physical observing parameters of Jupiter.
func JupiterPhysicalN(jd float64, n int) PlanetPhysicalInfo {
return planetPhysicalN(jd, n, jupiterPhysicalModel)
}
// SaturnPhysical 土星物理观测参数 / physical observing parameters of Saturn.
func SaturnPhysical(jd float64) PlanetPhysicalInfo {
return SaturnPhysicalN(jd, -1)
}
// SaturnPhysicalN 土星物理观测参数(截断版) / truncated physical observing parameters of Saturn.
func SaturnPhysicalN(jd float64, n int) PlanetPhysicalInfo {
return planetPhysicalN(jd, n, saturnPhysicalModel)
}
// UranusPhysical 天王星物理观测参数 / physical observing parameters of Uranus.
func UranusPhysical(jd float64) PlanetPhysicalInfo {
return UranusPhysicalN(jd, -1)
}
// UranusPhysicalN 天王星物理观测参数(截断版) / truncated physical observing parameters of Uranus.
func UranusPhysicalN(jd float64, n int) PlanetPhysicalInfo {
return planetPhysicalN(jd, n, uranusPhysicalModel)
}
// NeptunePhysical 海王星物理观测参数 / physical observing parameters of Neptune.
func NeptunePhysical(jd float64) PlanetPhysicalInfo {
return NeptunePhysicalN(jd, -1)
}
// NeptunePhysicalN 海王星物理观测参数(截断版) / truncated physical observing parameters of Neptune.
func NeptunePhysicalN(jd float64, n int) PlanetPhysicalInfo {
return planetPhysicalN(jd, n, neptunePhysicalModel)
}
func planetPhysicalN(jd float64, n int, model planetPhysicalModel) PlanetPhysicalInfo {
initialX, initialY, initialZ := planetXYZN(model.planetIndex, jd, n)
targetVector := Vector3{initialX, initialY, initialZ}
lightTimeDays := astronomicalUnitLightTimeDays * vectorMagnitude(targetVector)
targetJD := jd - lightTimeDays
geoX, geoY, geoZ := planetXYZN(model.planetIndex, targetJD, n)
geocentricVector := Vector3{geoX, geoY, geoZ}
observerDirection := normalizeVector(Vector3{-geocentricVector[0], -geocentricVector[1], -geocentricVector[2]})
heliocentricLongitude := planet.WherePlanetN(model.planetIndex, 0, targetJD, n)
heliocentricLatitude := planet.WherePlanetN(model.planetIndex, 1, targetJD, n)
solarDirection := normalizeVector(eclipticCartesian(heliocentricLongitude+180, -heliocentricLatitude, 1))
obliquity := EclipticObliquity(targetJD, false)
observerEquatorial := normalizeVector(rotateEclipticToEquatorial(observerDirection, obliquity))
solarEquatorial := normalizeVector(rotateEclipticToEquatorial(solarDirection, obliquity))
poleRA, poleDec, rotationEast := model.poleRotation(targetJD)
poleJ2000 := raDecToVector(poleRA, poleDec)
nodeJ2000 := Vector3{-math.Sin(poleRA * rad), math.Cos(poleRA * rad), 0}
eastJ2000 := normalizeVector(pxp(poleJ2000, nodeJ2000))
primeMeridianJ2000 := normalizeVector(Vector3{
nodeJ2000[0]*Cos(rotationEast) + eastJ2000[0]*Sin(rotationEast),
nodeJ2000[1]*Cos(rotationEast) + eastJ2000[1]*Sin(rotationEast),
nodeJ2000[2]*Cos(rotationEast) + eastJ2000[2]*Sin(rotationEast),
})
j2000ToDate := precessionMatrix(2451545.0, targetJD)
poleDate := normalizeVector(applyMatrix3(j2000ToDate, poleJ2000))
primeMeridianDate := normalizeVector(applyMatrix3(j2000ToDate, primeMeridianJ2000))
eastDate := normalizeVector(pxp(poleDate, primeMeridianDate))
poleRAOfDate, poleDecOfDate := vectorToRaDec(poleDate)
planetRA, planetDec := vectorToRaDec(observerEquatorial)
return PlanetPhysicalInfo{
SubEarthLongitude: bodyLongitude(observerEquatorial, primeMeridianDate, eastDate, model.positiveEast),
SubEarthLatitude: bodyLatitude(observerEquatorial, poleDate, model.equatorialRadius, model.polarRadius),
SubSolarLongitude: bodyLongitude(solarEquatorial, primeMeridianDate, eastDate, model.positiveEast),
SubSolarLatitude: bodyLatitude(solarEquatorial, poleDate, model.equatorialRadius, model.polarRadius),
NorthPolePositionAngle: northPolePositionAngle(planetRA, planetDec, poleRAOfDate, poleDecOfDate),
}
}
func bodyLongitude(direction, primeMeridian, eastAxis Vector3, positiveEast bool) float64 {
eastLongitude := Limit360(math.Atan2(vectorDot(direction, eastAxis), vectorDot(direction, primeMeridian)) * deg)
if positiveEast {
return eastLongitude
}
return Limit360(360 - eastLongitude)
}
func bodyLatitude(direction, pole Vector3, equatorialRadius, polarRadius float64) float64 {
geocentricLatitude := ArcSin(vectorDot(direction, pole))
if equatorialRadius == polarRadius {
return geocentricLatitude
}
ratio := (polarRadius * polarRadius) / (equatorialRadius * equatorialRadius)
return math.Atan2(math.Sin(geocentricLatitude*rad), math.Cos(geocentricLatitude*rad)*ratio) * deg
}
func northPolePositionAngle(planetRA, planetDec, poleRA, poleDec float64) float64 {
y := math.Cos(poleDec*rad) * math.Sin((poleRA-planetRA)*rad)
x := math.Sin(poleDec*rad)*math.Cos(planetDec*rad) -
math.Cos(poleDec*rad)*math.Sin(planetDec*rad)*math.Cos((poleRA-planetRA)*rad)
return Limit360(-math.Atan2(y, x) * deg)
}
func vectorDot(a, b Vector3) float64 {
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
}
func vectorMagnitude(vector Vector3) float64 {
return math.Sqrt(vectorDot(vector, vector))
}
func normalizeVector(vector Vector3) Vector3 {
normalized, magnitude := pn(vector)
if magnitude == 0 {
return Vector3{}
}
return normalized
}
func mercuryPoleRotation(jd float64) (ra, dec, rotationEast float64) {
days := jd - 2451545.0
julianCentury := days / 36525.0
m1 := Limit360(174.7910857 + 4.092335*days)
m2 := Limit360(349.5821714 + 8.184670*days)
m3 := Limit360(164.3732571 + 12.277005*days)
m4 := Limit360(339.1643429 + 16.369340*days)
m5 := Limit360(153.9554286 + 20.461675*days)
ra = 281.0103 - 0.0328*julianCentury
dec = 61.4155 - 0.0049*julianCentury
rotationEast = 329.5988 + 6.1385108*days +
0.01067257*Sin(m1) -
0.00112309*Sin(m2) -
0.00011040*Sin(m3) -
0.00002539*Sin(m4) -
0.00000571*Sin(m5)
return
}
func venusPoleRotation(jd float64) (ra, dec, rotationEast float64) {
days := jd - 2451545.0
return 272.76, 67.16, 160.20 - 1.4813688*days
}
// Mars/Jupiter orientation terms follow the official NAIF text PCK `pck00011.tpc`.
var (
marsPoleRAAngles = [...]phaseAngleModel{
{198.991226, 19139.4819985, 0},
{226.292679, 38280.8511281, 0},
{249.663391, 57420.7251593, 0},
{266.183510, 76560.6367950, 0},
{79.398797, 0.5042615, 0},
}
marsPoleRACoefficients = [...]float64{0.000068, 0.000238, 0.000052, 0.000009, 0.419057}
marsPoleDECAngles = [...]phaseAngleModel{
{122.433576, 19139.9407476, 0},
{43.058401, 38280.8753272, 0},
{57.663379, 57420.7517205, 0},
{79.476401, 76560.6495004, 0},
{166.325722, 0.5042615, 0},
}
marsPoleDECCoefficients = [...]float64{0.000051, 0.000141, 0.000031, 0.000005, 1.591274}
marsPrimeMeridianAngles = [...]phaseAngleModel{
{129.071773, 19140.0328244, 0},
{36.352167, 38281.0473591, 0},
{56.668646, 57420.9295360, 0},
{67.364003, 76560.2552215, 0},
{104.792680, 95700.4387578, 0},
{95.391654, 0.5042615, 0},
}
marsPrimeMeridianCoefficients = [...]float64{0.000145, 0.000157, 0.000040, 0.000001, 0.000001, 0.584542}
jupiterPoleAngles = [...]phaseAngleModel{
{99.360714, 4850.4046, 0},
{175.895369, 1191.9605, 0},
{300.323162, 262.5475, 0},
{114.012305, 6070.2476, 0},
{49.511251, 64.3000, 0},
}
jupiterPoleRACoefficients = [...]float64{0.000117, 0.000938, 0.001432, 0.000030, 0.002150}
jupiterPoleDECCoefficients = [...]float64{0.000050, 0.000404, 0.000617, -0.000013, 0.000926}
)
func marsPoleRotation(jd float64) (ra, dec, rotationEast float64) {
days := jd - 2451545.0
julianCentury := days / 36525.0
ra = 317.269202 - 0.10927547*julianCentury + sumSinOrientationTerms(marsPoleRAAngles[:], marsPoleRACoefficients[:], julianCentury)
dec = 54.432516 - 0.05827105*julianCentury + sumCosOrientationTerms(marsPoleDECAngles[:], marsPoleDECCoefficients[:], julianCentury)
rotationEast = 176.049863 + 350.891982443297*days +
sumSinOrientationTerms(marsPrimeMeridianAngles[:], marsPrimeMeridianCoefficients[:], julianCentury)
return
}
func jupiterPoleRotation(jd float64) (ra, dec, rotationEast float64) {
days := jd - 2451545.0
julianCentury := days / 36525.0
ra = 268.056595 - 0.006499*julianCentury + sumSinOrientationTerms(jupiterPoleAngles[:], jupiterPoleRACoefficients[:], julianCentury)
dec = 64.495303 + 0.002413*julianCentury + sumCosOrientationTerms(jupiterPoleAngles[:], jupiterPoleDECCoefficients[:], julianCentury)
rotationEast = 284.95 + 870.5360000*days
return
}
func saturnPoleRotation(jd float64) (ra, dec, rotationEast float64) {
days := jd - 2451545.0
julianCentury := days / 36525.0
ra = 40.589 - 0.036*julianCentury
dec = 83.537 - 0.004*julianCentury
rotationEast = 38.90 + 810.7939024*days
return
}
func uranusPoleRotation(jd float64) (ra, dec, rotationEast float64) {
days := jd - 2451545.0
return 257.311, -15.175, 203.81 - 501.1600928*days
}
func neptunePoleRotation(jd float64) (ra, dec, rotationEast float64) {
days := jd - 2451545.0
julianCentury := days / 36525.0
n := Limit360(357.85 + 52.316*julianCentury)
ra = 299.36 + 0.70*Sin(n)
dec = 43.46 - 0.51*Cos(n)
rotationEast = 249.978 + 541.1397757*days - 0.48*Sin(n)
return
}
func (model phaseAngleModel) at(julianCentury float64) float64 {
return Limit360(model.base + model.rate*julianCentury + model.accelRate*julianCentury*julianCentury)
}
func sumSinOrientationTerms(angles []phaseAngleModel, coefficients []float64, julianCentury float64) float64 {
sum := 0.0
for i, angle := range angles {
sum += coefficients[i] * Sin(angle.at(julianCentury))
}
return sum
}
func sumCosOrientationTerms(angles []phaseAngleModel, coefficients []float64, julianCentury float64) float64 {
sum := 0.0
for i, angle := range angles {
sum += coefficients[i] * Cos(angle.at(julianCentury))
}
return sum
}
+141
View File
@@ -0,0 +1,141 @@
package basic
import (
"encoding/json"
"math"
"os"
"testing"
"time"
)
type planetPhysicalSample struct {
Body string `json:"body"`
InputUTC string `json:"input_utc"`
SubEarthLongitude float64 `json:"sub_earth_longitude"`
SubEarthLatitude float64 `json:"sub_earth_latitude"`
SubSolarLongitude float64 `json:"sub_solar_longitude"`
SubSolarLatitude float64 `json:"sub_solar_latitude"`
NorthPolePositionAngle float64 `json:"north_pole_position_angle"`
}
func TestPlanetPhysicalMatchesHorizonsBaseline(t *testing.T) {
data, err := os.ReadFile("testdata/planet_physical_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []planetPhysicalSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
cases := map[string]func(float64) PlanetPhysicalInfo{
"mercury": MercuryPhysical,
"venus": VenusPhysical,
"mars": MarsPhysical,
"jupiter": JupiterPhysical,
"saturn": SaturnPhysical,
"uranus": UranusPhysical,
"neptune": NeptunePhysical,
}
for _, sample := range samples {
physical := cases[sample.Body]
if physical == nil {
t.Fatalf("missing body case %q", sample.Body)
}
date, err := time.Parse(time.RFC3339, sample.InputUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.InputUTC, err)
}
jd := TD2UT(Date2JDE(date.UTC()), true)
got := physical(jd)
assertPlanetPhaseClose(t, sample.Body+"."+sample.InputUTC+".SubEarthLongitude", got.SubEarthLongitude, sample.SubEarthLongitude, 0.02)
assertPlanetPhaseClose(t, sample.Body+"."+sample.InputUTC+".SubEarthLatitude", got.SubEarthLatitude, sample.SubEarthLatitude, 0.02)
assertPlanetPhaseClose(t, sample.Body+"."+sample.InputUTC+".SubSolarLongitude", got.SubSolarLongitude, sample.SubSolarLongitude, 0.02)
assertPlanetPhaseClose(t, sample.Body+"."+sample.InputUTC+".SubSolarLatitude", got.SubSolarLatitude, sample.SubSolarLatitude, 0.02)
assertPlanetPhaseClose(t, sample.Body+"."+sample.InputUTC+".NorthPolePositionAngle", got.NorthPolePositionAngle, sample.NorthPolePositionAngle, 0.02)
}
}
func TestPlanetPhysicalNFullMatchesDefault(t *testing.T) {
jd := TD2UT(Date2JDE(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)), true)
cases := []struct {
name string
physical func(float64) PlanetPhysicalInfo
physicalN func(float64, int) PlanetPhysicalInfo
}{
{"Mercury", MercuryPhysical, MercuryPhysicalN},
{"Venus", VenusPhysical, VenusPhysicalN},
{"Mars", MarsPhysical, MarsPhysicalN},
{"Jupiter", JupiterPhysical, JupiterPhysicalN},
{"Saturn", SaturnPhysical, SaturnPhysicalN},
{"Uranus", UranusPhysical, UranusPhysicalN},
{"Neptune", NeptunePhysical, NeptunePhysicalN},
}
for _, tc := range cases {
got := tc.physical(jd)
gotN := tc.physicalN(jd, -1)
assertSameFloat(t, tc.name+".SubEarthLongitude", got.SubEarthLongitude, gotN.SubEarthLongitude)
assertSameFloat(t, tc.name+".SubEarthLatitude", got.SubEarthLatitude, gotN.SubEarthLatitude)
assertSameFloat(t, tc.name+".SubSolarLongitude", got.SubSolarLongitude, gotN.SubSolarLongitude)
assertSameFloat(t, tc.name+".SubSolarLatitude", got.SubSolarLatitude, gotN.SubSolarLatitude)
assertSameFloat(t, tc.name+".NorthPolePositionAngle", got.NorthPolePositionAngle, gotN.NorthPolePositionAngle)
}
}
func TestPlanetPhysicalSampleSweepFiniteAndInRange(t *testing.T) {
dates := []time.Time{
time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC),
time.Date(1969, 7, 20, 20, 17, 40, 0, time.UTC),
time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC),
time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC),
time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC),
}
cases := []struct {
name string
physical func(float64) PlanetPhysicalInfo
}{
{"Mercury", MercuryPhysical},
{"Venus", VenusPhysical},
{"Mars", MarsPhysical},
{"Jupiter", JupiterPhysical},
{"Saturn", SaturnPhysical},
{"Uranus", UranusPhysical},
{"Neptune", NeptunePhysical},
}
for _, date := range dates {
jd := TD2UT(Date2JDE(date.UTC()), true)
for _, tc := range cases {
info := tc.physical(jd)
prefix := tc.name + "." + date.Format(time.RFC3339)
assertFiniteRange(t, prefix+".SubEarthLongitude", info.SubEarthLongitude, 0, 360, true)
assertFiniteRange(t, prefix+".SubEarthLatitude", info.SubEarthLatitude, -90, 90, false)
assertFiniteRange(t, prefix+".SubSolarLongitude", info.SubSolarLongitude, 0, 360, true)
assertFiniteRange(t, prefix+".SubSolarLatitude", info.SubSolarLatitude, -90, 90, false)
assertFiniteRange(t, prefix+".NorthPolePositionAngle", info.NorthPolePositionAngle, 0, 360, true)
}
}
}
func assertFiniteRange(t *testing.T, name string, got, min, max float64, upperExclusive bool) {
t.Helper()
if math.IsNaN(got) || math.IsInf(got, 0) {
t.Fatalf("%s is not finite: %.18f", name, got)
}
if upperExclusive {
if got < min || got >= max {
t.Fatalf("%s out of range: %.18f not in [%.18f, %.18f)", name, got, min, max)
}
return
}
if got < min || got > max {
t.Fatalf("%s out of range: %.18f not in [%.18f, %.18f]", name, got, min, max)
}
}
+110
View File
@@ -0,0 +1,110 @@
package basic
import (
"encoding/json"
"os"
"testing"
"time"
)
type planetRiseSetBaselineSample struct {
Body string `json:"body"`
Site string `json:"site"`
InputUTC string `json:"input_utc"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
RiseUTC string `json:"rise_utc"`
TransitUTC string `json:"transit_utc"`
SetUTC string `json:"set_utc"`
}
func TestPlanetRiseSetMatchesHorizonsBaseline(t *testing.T) {
data, err := os.ReadFile("testdata/planet_rise_set_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []planetRiseSetBaselineSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
type observationCase struct {
rise func(float64, float64, float64, float64, float64, float64) (float64, error)
transit func(float64, float64, float64) float64
set func(float64, float64, float64, float64, float64, float64) (float64, error)
}
cases := map[string]observationCase{
"mercury": {rise: MercuryRiseTime, transit: MercuryCulminationTime, set: MercurySetTime},
"venus": {rise: VenusRiseTime, transit: VenusCulminationTime, set: VenusSetTime},
"mars": {rise: MarsRiseTime, transit: MarsCulminationTime, set: MarsSetTime},
"jupiter": {rise: JupiterRiseTime, transit: JupiterCulminationTime, set: JupiterSetTime},
"saturn": {rise: SaturnRiseTime, transit: SaturnCulminationTime, set: SaturnSetTime},
"uranus": {rise: UranusRiseTime, transit: UranusCulminationTime, set: UranusSetTime},
"neptune": {rise: NeptuneRiseTime, transit: NeptuneCulminationTime, set: NeptuneSetTime},
}
const tolerance = 2 * time.Minute
var maxRiseDiff time.Duration
var maxTransitDiff time.Duration
var maxSetDiff time.Duration
for _, sample := range samples {
tc, ok := cases[sample.Body]
if !ok {
t.Fatalf("unknown body %q", sample.Body)
}
inputTime, err := time.Parse(time.RFC3339, sample.InputUTC)
if err != nil {
t.Fatalf("parse input time %q: %v", sample.InputUTC, err)
}
jd := Date2JDE(inputTime.UTC())
riseJD, err := tc.rise(jd, sample.Longitude, sample.Latitude, 0, 1, 0)
if err != nil {
t.Fatalf("%s %s rise error: %v", sample.Body, sample.Site, err)
}
riseDiff := assertEventTimeClose(t, sample.Body+"."+sample.Site+".rise", riseJD, sample.RiseUTC, tolerance)
if riseDiff > maxRiseDiff {
maxRiseDiff = riseDiff
}
transitJD := tc.transit(jd, sample.Longitude, 0)
transitDiff := assertEventTimeClose(t, sample.Body+"."+sample.Site+".transit", transitJD, sample.TransitUTC, tolerance)
if transitDiff > maxTransitDiff {
maxTransitDiff = transitDiff
}
setJD, err := tc.set(jd, sample.Longitude, sample.Latitude, 0, 1, 0)
if err != nil {
t.Fatalf("%s %s set error: %v", sample.Body, sample.Site, err)
}
setDiff := assertEventTimeClose(t, sample.Body+"."+sample.Site+".set", setJD, sample.SetUTC, tolerance)
if setDiff > maxSetDiff {
maxSetDiff = setDiff
}
}
t.Logf("planet rise/set max diff: rise=%v transit=%v set=%v", maxRiseDiff, maxTransitDiff, maxSetDiff)
}
func assertEventTimeClose(t *testing.T, name string, gotJD float64, wantUTC string, tolerance time.Duration) time.Duration {
t.Helper()
wantTime, err := time.Parse(time.RFC3339, wantUTC)
if err != nil {
t.Fatalf("parse %s baseline time %q: %v", name, wantUTC, err)
}
gotTime := JDE2DateByZone(gotJD, time.UTC, false)
diff := gotTime.Sub(wantTime)
if diff < 0 {
diff = -diff
}
if diff > tolerance {
t.Fatalf("%s mismatch: got %s want %s tolerance %v", name, gotTime.Format(time.RFC3339), wantUTC, tolerance)
}
return diff
}

Some files were not shown because too many files have changed in this diff Show More