24 Commits

Author SHA1 Message Date
b612 16c62a97d5 feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
2026-09-23 18:55:12 +08:00
b612 1f31a9b5b5 docs: 修正README中月球公式计算口径表述笔误 2026-09-17 21:33:28 +08:00
b612 2bf8478639 feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑
- 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口
- 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界
- 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名
- 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验
- 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口
- 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
2026-09-17 12:27:40 +08:00
b612 9ee2163cc7 feat: 新增月掩与日月食地理绘图并提升观测计算精度
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算
- 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出
- 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口
- 扩展日食中心线、南北界及偏食足迹采样,支持极区投影
- 修正站心时角、月出月落、月球视半径、折射和恒星自行计算
- 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
2026-08-06 12:00:56 +08:00
b612 25dc7ac0bc calendar: 补齐前104古历纪年解析与回归测试
- 为先秦与秦汉古历结果填充周、鲁、秦、西汉早期纪年信息
- 支持默认与显式古历下的年号日期解析
2026-06-11 09:33:46 +08:00
b612 a8e7513683 • feat(calendar): 扩展先秦至秦汉古历支持
- 新增显式古历 API,支持先秦古历与秦汉颛顼历选择
- 将默认公农历转换范围扩展至 -721..3000
- 支持后九月解析、负年份干支日和古历法相符节气
- 补充秦汉、先秦、交接边界和节气回归测试
2026-06-09 19:35:18 +08:00
b612 c8dd777a7b docs: 统一公开 API 的中英双语注释
- 补齐公开接口说明段的英文描述,保持签名注释和详细说明均为中英双语结构
- 规范农历、坐标、公式、轨道、日晷、太阳、恒星及行星事件等 API 的注释口径
2026-05-27 16:08:11 +08:00
b612 46b555cd49 fix: 修复天象事件 API 在事件边界附近的重复返回问题
- 修正合月、合日、留等Last/Next/Closest接口在精确命中和事件后秒级查询时仍返回当前事件的问题,避免应用侧枚举卡死
- 收紧事件边界判定,并补强水星、金星、火星近事件路径的稳定化,保证公开API的单调前进语义
- 补充公开wrapper、跨世纪样本和外部基线回归,覆盖合月邻近事件与边界场景
2026-05-23 23:08:05 +08:00
b612 be3af3884c feat(moon): 新增行星合月查询并修正月球地心赤经赤纬接口
- 修正月球地心真/视赤经赤纬接口口径
- 新增月球与七大行星合月时刻查询
2026-05-23 19:00:53 +08:00
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
b612 98ff574495 bug fix:修正公农历历算问题 2026-02-20 12:11:38 +08:00
b612 dadbba7171 improve:月升月落算法优化 2026-02-17 23:23:52 +08:00
b612 add07bbd85 add deltaT func to astro.go 2026-02-16 13:20:22 +08:00
b612 27b7e4ab77 年号异体字修正 2025-11-02 12:01:04 +08:00
b612 0ab91bcd2d fix:修正百年闰年2月29日处理过程 2025-10-04 17:00:19 +08:00
b612 543abcafa5 更换岁差、章动算法 2025-09-18 13:16:04 +08:00
b612 9f688024e8 1.修正:农历年返回不正确的问题 2025-09-16 11:48:54 +08:00
b612 b865e64fa9 1. 补充农历覆盖到公元前104年
2. 修正年号问题
2025-09-16 00:11:10 +08:00
b612 3389c33cb5 1. 补充农历覆盖到公元前104年
2. 修正时区导致的计算问题
2025-09-15 23:40:09 +08:00
b612 d6b6452304 1. 补充农历覆盖到公元前104年
2. 农历信息中包含君主、年号、朝代
3. 丰富一些算法,修正一些问题
2025-09-15 20:55:38 +08:00
b612 438f1700c7 - fix: Correct sun.TrueBo calculation, now using basic.HSunTrueBo for correct solar latitude
- fix: Rename GetSunDownTime to GetSunSetTime in basic/sun.go and update related calls/outputs
- fix: Update test cases to use new method and error constant names

- improve: Rename all DownTime/Down functions to SetTime/Set for consistency
- improve: Standardize ERR_XXX_NEVER_DOWN error constants to ERR_XXX_NEVER_SET, with ERR_XXX_NEVER_DOWN kept as compatibility alias
- improve: More standard naming for interfaces and errors to improve maintainability and readability
2025-09-08 10:59:04 +08:00
854 changed files with 221914 additions and 18145 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.
+109
View File
@@ -0,0 +1,109 @@
# Astro
**English | [中文](README.md)**
[![Go Reference](https://pkg.go.dev/badge/b612.me/astro.svg)](https://pkg.go.dev/b612.me/astro)
An astronomy library I have used for years for my own interest in astronomy and calendars.
Based on Jean Meeus's *Astronomical Algorithms*. Solar and planetary calculations use built-in VSOP87 terms; lunar calculations use ELP2000/82-style truncated series. No external ephemeris files are required.
Intended for calendar calculations, amateur observation and studying astronomical algorithms.
## Install
```sh
go get b612.me/astro
```
## Highlights
- Civil and Chinese calendar conversion from 721 BCE to 3000 CE, solar terms, sexagenary dates, era names and historical calendars.
- Positions, rise/set, transit, distances, angular diameters and physical ephemerides for the Sun, Moon and seven planets; lightweight Sun and Moon algorithms are also available.
- Solar and lunar eclipses, stellar and planetary lunar occultations, local contacts and global paths.
- SVG charts, GeoJSON data and KML for viewing and time playback in Google Earth.
- A built-in catalog of 9,100 stars, with constellation lookup, coordinate corrections and rise/set calculations.
- Coordinate transformations, sidereal time, precession, nutation, refraction, small-body orbits, sundials and astronomical formulas.
## Example
Calculate sunrise in Xi'an, the lunar phase and the Chinese calendar date:
```go
package main
import (
"fmt"
"log"
"time"
"b612.me/astro/calendar"
"b612.me/astro/moon"
"b612.me/astro/sun"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
date := time.Date(2026, 2, 17, 12, 0, 0, 0, cst)
rise, err := sun.RiseTime(date, 108.93, 34.27, 0, true)
if err != nil {
log.Fatal(err)
}
fmt.Println("Sunrise:", rise.Format("15:04:05"))
fmt.Println("Lunar phase:", moon.PhaseDesc(date))
day, err := calendar.SolarToLunar(date)
if err != nil {
log.Fatal(err)
}
fmt.Println("Chinese calendar:", day.Lunar().MonthDay())
}
```
Longitude and latitude are in degrees, positive east and north. The example uses a height of 0 metres and enables refraction with `true`. Rise/set functions return errors for polar conditions or a missing event on that date.
Calendar and phase descriptions in this example are returned in Chinese.
## Package Overview
| Package | Purpose and documentation |
| --- | --- |
| `calendar` | [Calendar conversion, solar terms, sexagenary dates and historical calendars](doc/manual/en/calendar.md) |
| `sun` / `moon` / `lite/sun` / `lite/moon` | [Sun and Moon positions, rise/set, phases and physical quantities](doc/manual/en/sun-moon.md) |
| `mercury` / `venus` / `mars` / `jupiter` / `saturn` / `uranus` / `neptune` / `earth` | [Planetary positions, events, Jupiter's satellites and Saturn's rings](doc/manual/en/planets.md) |
| `eclipse` / `eclipse/svg` | [Eclipse searches, local visibility, paths and charts](doc/manual/en/eclipse.md) |
| `moon` / `moon/svg` | [Stellar and planetary lunar occultations, contacts and bands](doc/manual/en/occultation.md) |
| `geojson` / `kml` | [Event maps, GeoJSON and KML](doc/manual/en/map-geojson.md) |
| `star` | [Star catalog, constellations and observing quantities](doc/manual/en/star.md) |
| `coord` | [Coordinates, sidereal time, precession, nutation and refraction](doc/manual/en/coord.md) |
| `orbit` | [Asteroid and comet two-body orbits, visual binaries](doc/manual/en/orbit.md) |
| `sundial` | [Apparent solar time and sundial geometry](doc/manual/en/sundial.md) |
| `formula` | [Radiation, magnitudes, synodic periods and telescope formulas](doc/manual/en/formula.md) |
| `astro` (root) | [UTC, UT1, TT and ΔT models](doc/manual/en/timescale.md) |
`basic` contains the underlying algorithms, `planet` holds analytical series, and `tools` provides numerical helpers. Most applications can use the packages above.
Full signatures are also available in the [Go API documentation](https://pkg.go.dev/b612.me/astro).
## Time Scale Conventions
Most observing APIs accept a civil instant as `time.Time` and handle UTC, UT1 and TT conversions internally. Before 1972 the library treats civil time as UT1, and as UTC thereafter. Orbital epochs and some low-level APIs have separate scale requirements.
Angles default to degrees, angular diameters to arcseconds, sidereal time to hours, and distances to AU or km as specified by the API. SVG and GeoJSON time labels default to UTC, with explicit UT1 options.
See [Time scales](doc/manual/en/timescale.md).
DUT1 and TT−UTC default to extrapolating the current leap-second rule beyond the observed window (through September 2026); `SetTimeScaleFuturePolicy` switches the TT−UTC convention to one of four alternatives: a frozen offset, continued UT1 tracking, the leap-hour rule, or a civil scale permanently identical to UT1.
## Observer Height Convention
Observer height is ellipsoidal height in metres. Convert orthometric height `H` using the local geoid undulation `N`: `height = H + N`. See [Observer height](doc/manual/en/coord.md#observer-height).
## Scope And Accuracy
The library uses analytical models and truncated series. Accuracy varies with date, body and calculation. Professional occultation predictions and spacecraft navigation require more precise ephemerides and physical models.
`lite/sun` and `lite/moon` suit resource-constrained applications. Some main APIs also provide truncated variants with an `N` suffix.
Model ranges, measured differences and benchmarks are documented under [Accuracy and performance](doc/manual/en/accuracy.md).
+99
View File
@@ -0,0 +1,99 @@
# Astro
**[English](README.en.md) | 中文**
[![Go Reference](https://pkg.go.dev/badge/b612.me/astro.svg)](https://pkg.go.dev/b612.me/astro)
自用多年的天文算法库,用于个人天文历法爱好。
基于 Jean Meeus《天文算法》(Astronomical Algorithms)实现。太阳和行星使用内置 VSOP87 解析项,月球使用 ELP2000/82 风格截断级数,不依赖外部星历文件。适合历法计算、业余观测和天文算法学习。
## 安装
```sh
go get b612.me/astro
```
## 功能概览
- 公历与农历互转,支持公元前 721 年至公元 3000 年;节气、干支、年号与古历。
- 太阳、月亮、七大行星的位置、升落、中天、距离、视直径与物理星历;另有轻量日月算法。
- 日月食、恒星与行星月掩的事件搜索、地方接触时刻和全球路径。
- SVG 天象图、GeoJSON 地理数据和可供 Google Earth 查看及按时间播放的 KML。
- 内置 9100 颗恒星,支持星座判定、坐标修正与升落计算。
- 天球坐标转换、恒星时、岁差章动、折射、通用小天体轨道、日晷和常用天文公式。
## 使用示例
计算西安某日的日出时刻、月相,以及当天的农历日期:
```go
package main
import (
"fmt"
"log"
"time"
"b612.me/astro/calendar"
"b612.me/astro/moon"
"b612.me/astro/sun"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
date := time.Date(2026, 2, 17, 12, 0, 0, 0, cst)
rise, err := sun.RiseTime(date, 108.93, 34.27, 0, true)
if err != nil {
log.Fatal(err)
}
fmt.Println("日出:", rise.Format("15:04:05"))
fmt.Println("月相:", moon.PhaseDesc(date))
day, err := calendar.SolarToLunar(date)
if err != nil {
log.Fatal(err)
}
fmt.Println("农历:", day.Lunar().MonthDay())
}
```
经纬度以东经、北纬为正,单位为度;示例高度为 0 米,`true` 表示升落计算考虑大气折射。极昼、极夜或当天没有升落事件时,升落接口会返回错误。
## 包概览
| 包 | 用途与文档 |
| --- | --- |
| `calendar` | [历法转换、节气、干支与古历](doc/manual/calendar.md) |
| `sun` / `moon` / `lite/sun` / `lite/moon` | [太阳与月亮的位置、升落、月相和物理量](doc/manual/sun-moon.md) |
| `mercury` / `venus` / `mars` / `jupiter` / `saturn` / `uranus` / `neptune` / `earth` | [行星位置、特殊天象、木星卫星与土星环](doc/manual/planets.md) |
| `eclipse` / `eclipse/svg` | [日月食查询、地方可见性、路径与出图](doc/manual/eclipse.md) |
| `moon` / `moon/svg` | [恒星及行星月掩、接触时刻与掩带](doc/manual/occultation.md) |
| `geojson` / `kml` | [天象地图、GeoJSON 与 KML](doc/manual/map-geojson.md) |
| `star` | [恒星数据库、星座与观测量](doc/manual/star.md) |
| `coord` | [坐标转换、恒星时、岁差、章动与折射](doc/manual/coord.md) |
| `orbit` | [小行星、彗星等二体轨道与视双星](doc/manual/orbit.md) |
| `sundial` | [真太阳时与日晷几何](doc/manual/sundial.md) |
| `formula` | [辐射、星等、会合周期与望远镜公式](doc/manual/formula.md) |
| `astro`(根包) | [UTC、UT1、TT 换算与 ΔT 模型](doc/manual/timescale.md) |
`basic` 是底层算法包,`planet` 保存解析级数,`tools` 提供数值辅助函数。一般使用上表中的包即可;完整函数签名也可查 [Go API 文档](https://pkg.go.dev/b612.me/astro)。
## 时标约定
一般观测接口接收表示民用时刻的 `time.Time`,内部处理 UTC、UT1 与 TT 换算。1972 年以前,库将民用时间按 UT1 处理,之后按UTC处理;轨道历元和部分底层接口另有时标要求。
角度默认用度,视直径用角秒,恒星时用小时,距离按接口使用 AU 或 km。SVG 和 GeoJSON 默认输出 UTC 时间标签,也可显式选择 UT1。具体约定见[时标手册](doc/manual/timescale.md)。
DUT1 与 TT−UTC 在实测窗口(截至 2026 年 9 月)之后默认沿用现行闰秒规则外推,可用 `SetTimeScaleFuturePolicy` 调整 TT−UTC 口径到"冻结偏移量"、"继续跟随 UT1"、"闰时规则"、"持续等价UT1"四个规则。
## 观测点高度约定
观测点高度为椭球高,单位米。若只有海拔(正高)`H`,应结合当地大地水准面差距 `N` 换算:`height = H + N`。详见[观测点高度](doc/manual/coord.md#观测点高度)。
## 适用范围与精度
本库使用解析模型和截断级数。精度随年代、天体与计算项目变化;专业掩星、航天导航等用途需要更精确的星历和物理模型。
`lite/sun`、`lite/moon` 适合资源受限环境;部分主接口也提供带 `N` 后缀的截断版本。模型范围、误差样本和性能数据见[精度与性能](doc/manual/accuracy.md)。
+93 -1
View File
@@ -1,2 +1,94 @@
// Package astro
// Package astro 提供进程级时标配置与时标换算 / process-wide time-scale configuration and conversions.
package astro
import "b612.me/astro/basic"
// DeltaT 返回当前的 TT−UT1(ΔT)模型 / the active TT−UT1 (DeltaT) model.
func DeltaT() func(float64, bool) float64 {
return basic.GetDeltaTFn()
}
// SetDeltaT 替换 ΔT 模型(入参为数值与“该数值是否为儒略日”,为假时是十进制年),传 nil 恢复默认 / replaces the DeltaT model; nil restores the default.
func SetDeltaT(deltaT func(float64, bool) float64) {
basic.SetDeltaTFn(deltaT)
}
// DefaultDeltaT 返回内置默认 ΔT 模型 / the built-in default DeltaT model.
func DefaultDeltaT() func(float64, bool) float64 {
return basic.DefaultDeltaTv2
}
// TTMinusUTC 返回当前 TT−UTC 覆盖函数,未设置时为 nil / current TT−UTC override, or nil.
func TTMinusUTC() func(float64) float64 {
return basic.GetTTMinusUTCFn()
}
// SetTTMinusUTC 替换 TT−UTC 模型,nil 恢复内置表与政策 / overrides TT−UTC; nil restores the built-in table and policy.
func SetTTMinusUTC(ttMinusUTC func(float64) float64) {
basic.SetTTMinusUTCFn(ttMinusUTC)
}
// DefaultTTMinusUTC 返回内置闰秒表模型,忽略覆盖与未来政策 / built-in leap-table model, ignoring overrides and future policy.
func DefaultTTMinusUTC() func(float64) float64 {
return basic.TTMinusUTCSecondsDefault
}
// TimeScaleFuturePolicy 民用时标换算政策,与输出用的 TimeScale 无关 / civil-time conversion policy, separate from output TimeScale.
type TimeScaleFuturePolicy = basic.TimeScaleFuturePolicy
const (
// TimeScaleLeapSecond 默认按 ΔT 越限施加整数秒校正,不代表闰秒公告 / default integer-second corrections driven by extrapolated ΔT, not announcements.
TimeScaleLeapSecond = basic.TimeScaleLeapSecond
// TimeScaleAssumeUT1Tracking 窗口外固定末端 DUT1,平滑跟随 UT1 / holds the last observed DUT1 beyond the window.
TimeScaleAssumeUT1Tracking = basic.TimeScaleAssumeUT1Tracking
// TimeScaleFreezeUTCOffset 假设 UTC 偏移冻结在精确窗口末端 / freezes the UTC offset at the window end.
TimeScaleFreezeUTCOffset = basic.TimeScaleFreezeUTCOffset
// TimeScaleLeapHour 按 ΔT 越限施加整小时校正,仅作情景演算 / applies hour-sized corrections as a scenario assumption.
TimeScaleLeapHour = basic.TimeScaleLeapHour
// TimeScaleUT1Civil 全时轴民用时标等同 UT1,TT−UTC 覆盖仍优先 / uses UT1 as civil time everywhere unless TT−UTC is overridden.
TimeScaleUT1Civil = basic.TimeScaleUT1Civil
)
// SetTimeScaleFuturePolicy 设置与 basic 共享的民用时标换算政策 / sets the civil-time conversion policy shared with basic.
func SetTimeScaleFuturePolicy(policy TimeScaleFuturePolicy) {
basic.SetTimeScaleFuturePolicy(policy)
}
// GetTimeScaleFuturePolicy 返回当前民用时标换算政策 / current civil-time conversion policy.
func GetTimeScaleFuturePolicy() TimeScaleFuturePolicy {
return basic.GetTimeScaleFuturePolicy()
}
// DeltaTModel 标识可选的已发布 ΔT 模型 / a selectable published ΔT model.
type DeltaTModel = basic.DeltaTModel
const (
// DeltaTModelDefault 内置默认:SMH2016 + Morrison 2021 / the built-in SMH2016 + Morrison 2021 model.
DeltaTModelDefault = basic.DeltaTModelDefault
// DeltaTModelSMH2016 显式选择内置模型 / selects the built-in model explicitly.
DeltaTModelSMH2016 = basic.DeltaTModelSMH2016
// DeltaTModelMS2004 Morrison & Stephenson 2004 长期抛物线 / the M&S2004 long-term parabola.
DeltaTModelMS2004 = basic.DeltaTModelMS2004
// DeltaTModelEspenakMeeus2006 Espenak & Meeus 2006 分段多项式 / the Espenak & Meeus 2006 piecewise polynomial.
DeltaTModelEspenakMeeus2006 = basic.DeltaTModelEspenakMeeus2006
// DeltaTModelNASACanon2006 上式再加配对修正 c,即 NASA 五千年目录的印刷口径 / plus the pairing term c, the NASA canon convention.
DeltaTModelNASACanon2006 = basic.DeltaTModelNASACanon2006
// DeltaTModelManual 表示当前生效的是 SetDeltaT 注入的任意函数 / an arbitrary function injected with SetDeltaT.
DeltaTModelManual = basic.DeltaTModelManual
)
// SetDeltaTModel 安装命名 ΔT 模型;keepObserved 为真时逐月实测段优先(推荐),未知模型返回 false 且不改动现状。
// SetDeltaTModel installs a named ΔT model; with keepObserved the monthly observed span wins wherever it covers the instant.
func SetDeltaTModel(model DeltaTModel, keepObserved bool) bool {
return basic.SetDeltaTModel(model, keepObserved)
}
// GetDeltaTModel 返回当前生效的模型与是否保留实测段 / the active model and whether the observed span is kept.
func GetDeltaTModel() (DeltaTModel, bool) {
return basic.GetDeltaTModel()
}
// DeltaTModelSeconds 按命名模型求 ΔT(秒,入参为 UT 儒略日),不改变进程状态,便于对照 / evaluates a named model without touching process state.
func DeltaTModelSeconds(model DeltaTModel, jd float64, keepObserved bool) float64 {
return basic.DeltaTModelSeconds(model, jd, keepObserved)
}
+69
View File
@@ -0,0 +1,69 @@
package astro
import (
"math"
"testing"
"time"
"b612.me/astro/basic"
)
// time.Time 往返经 JD 舍入,容差为 0.2 ms。
func TestBarycentricTimeFacade(t *testing.T) {
tt := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
if got := TCGMinusTT(tt); math.Abs(got-1.0777) > 5e-3 {
t.Errorf("TCG−TT=%v 秒, want ≈1.078", got)
}
if got := TCBMinusTT(tt); math.Abs(got-23.9757) > 5e-3 {
t.Errorf("TCB−TT=%v 秒, want ≈23.976", got)
}
if got := TDBMinusTT(tt); math.Abs(got) > 2e-3 {
t.Errorf("TDB−TT=%v 秒, want |·| ≤ 1.7 ms", got)
}
for _, tc := range []struct {
name string
fwd func(time.Time) time.Time
back func(time.Time) time.Time
}{
{"TCG", TCGFromTT, TTFromTCG},
{"TCB", TCBFromTT, TTFromTCB},
{"TDB", TDBFromTT, TTFromTDB},
} {
round := tc.back(tc.fwd(tt))
if delta := math.Abs(round.Sub(tt).Seconds()); delta > 2e-4 {
t.Errorf("%s 往返差 %.6f 秒", tc.name, delta)
}
if forward := tc.fwd(tt); !forward.After(tt.Add(-time.Second)) || !forward.Before(tt.Add(time.Minute)) {
t.Errorf("%s 结果 %v 离开合理区间", tc.name, forward)
}
}
tcb := TCBFromTT(tt)
if diff := tcb.Sub(TDBFromTCB(tcb)).Seconds(); math.Abs(diff-23.9757) > 0.1 {
t.Errorf("TCB−TDB=%v 秒, want ≈23.976", diff)
}
if delta := math.Abs(TTFromTCB(tcb).Sub(tt).Seconds()); delta > 2e-4 {
t.Errorf("TT→TCB 往返差 %.6f 秒", delta)
}
}
func TestBarycentricTimeFacadePreservesScaleAndInstant(t *testing.T) {
tt := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
for _, date := range []time.Time{tt, tt.In(time.FixedZone("UTC+08", 8*3600))} {
jd := basic.Date2JD(date.UTC())
for name, pair := range map[string][2]float64{
"TCG": {TCGMinusTT(date), basic.TCGMinusTTSeconds(jd)},
"TCB": {TCBMinusTT(date), basic.TCBMinusTTSeconds(jd)},
"TDB": {TDBMinusTT(date), basic.TDBMinusTTSeconds(jd)},
} {
if pair[0] != pair[1] {
t.Errorf("%s facade=%v, basic=%v", name, pair[0], pair[1])
}
}
if direct, composed := TDBFromTT(date), TDBFromTCB(TCBFromTT(date)); math.Abs(direct.Sub(composed).Seconds()) > 1e-4 {
t.Errorf("direct TDB=%v, composed=%v", direct, composed)
}
if got := TCGFromTT(date); got.Location() != time.UTC || !got.Equal(TCGFromTT(tt)) {
t.Errorf("TCG depends on input Location: %v", got)
}
}
}
+262
View File
@@ -0,0 +1,262 @@
// 根包回归:testdata 的位级基线与 n 截断契约(n<0 用全项,n>=0 保留约 n 个主项)。
package astro_test
import (
"encoding/json"
"fmt"
"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
}
// truncationTermCounts 是截断检验抽取的 n 值。
var truncationTermCounts = []int{2, 4, 8, 16}
// angularDifference 两个角度量的最小夹角,单位度。
func angularDifference(a, b float64) float64 {
d := math.Mod(a-b, 360)
if d > 180 {
d -= 360
}
if d < -180 {
d += 360
}
return math.Abs(d)
}
// truncationCase 是一个截断探针:err 给出第 sample 个采样点在截断项数 n 下相对全项 n<0 的偏差。
type truncationCase struct {
name string
tol []float64
err func(sample, n int) float64
}
// scalarTruncation 构造标量探针;jd 按采样索引取值,angular 为真时偏差取角度最小夹角。
func scalarTruncation(name string, jd func(index int) float64, f func(float64, int) float64, angular bool, tol []float64) truncationCase {
return truncationCase{
name: name,
tol: tol,
err: func(sample, n int) float64 {
got := f(jd(sample), n)
full := f(jd(sample), -1)
if angular {
return angularDifference(got, full)
}
return math.Abs(got - full)
},
}
}
// pairTruncation 构造经纬成对返回的探针,取两个分量偏差的较大者。
func pairTruncation(name string, jd func(index int) float64, f func(float64, int) (float64, float64), tol []float64) truncationCase {
return truncationCase{
name: name,
tol: tol,
err: func(sample, n int) float64 {
gotA, gotB := f(jd(sample), n)
fullA, fullB := f(jd(sample), -1)
return math.Max(angularDifference(gotA, fullA), angularDifference(gotB, fullB))
},
}
}
// dateTruncation 构造以 time.Time 为入口的探针,偏差按角度最小夹角计。
func dateTruncation(name string, date func(index int) time.Time, f func(time.Time, int) float64, tol []float64) truncationCase {
return truncationCase{
name: name,
tol: tol,
err: func(sample, n int) float64 {
return angularDifference(f(date(sample), n), f(date(sample), -1))
},
}
}
// baselineDates 解析基线样本的 UTC 时刻。
func baselineDates(t *testing.T, samples []baselineSample) []time.Time {
t.Helper()
dates := make([]time.Time, len(samples))
for i, sample := range samples {
parsed, err := time.Parse(time.RFC3339Nano, sample.UTC)
if err != nil {
t.Fatal(err)
}
dates[i] = parsed
}
return dates
}
// assertTruncationConverges 逐采样点检验误差上限,并要求最坏误差随 n 不增、多数采样点严格下降。
// 单点截断误差不是嵌套部分和,允许同量级抖动,因此“不增”约束的是采样集上的误差包络。
func assertTruncationConverges(t *testing.T, cases []truncationCase, samples int) {
t.Helper()
for _, tc := range cases {
envelope := make([]float64, len(truncationTermCounts))
strict := 0
for sample := 0; sample < samples; sample++ {
first, last := 0.0, 0.0
for i, n := range truncationTermCounts {
e := tc.err(sample, n)
if e > tc.tol[i] {
t.Fatalf("%s: sample %d truncation error %.6g at n=%d exceeds %.6g", tc.name, sample, e, n, tc.tol[i])
}
if e > envelope[i] {
envelope[i] = e
}
if i == 0 {
first = e
}
last = e
}
if last < first {
strict++
}
}
for i := 1; i < len(envelope); i++ {
if envelope[i] > envelope[i-1] {
t.Fatalf("%s: worst truncation error grows from %.6g at n=%d to %.6g at n=%d",
tc.name, envelope[i-1], truncationTermCounts[i-1], envelope[i], truncationTermCounts[i])
}
}
if 2*strict < samples {
t.Fatalf("%s: truncation error shrank with n at only %d of %d samples", tc.name, strict, 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)
}
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)
}
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)
}
}
if math.Float64bits(basic.HMoonTrueLo(sample.TTJD)) != sample.Moon.LonBits {
t.Fatalf("moon lon 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.HMoonAway(sample.TTJD)) != sample.Moon.DisBits {
t.Fatalf("moon distance regression at %s", sample.UTC)
}
}
}
// planetTruncationName 与 planetTruncationTolerance 的下标同 WherePlanetN 的 xt。
var planetTruncationName = []string{"earth", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune"}
// planetTruncationTolerance 是 zn=黄经/黄纬(度)、日心距(AU)两种量纲的上限,取实测最坏值的 5 倍以上。
var planetTruncationTolerance = [][]float64{
{20, 2, 0.5, 0.1},
{5, 0.5, 0.05, 0.01},
{0.3, 0.1, 0.05, 0.02},
}
func planetMoonTruncationCases(samples []baselineSample) []truncationCase {
jdOf := func(index int) float64 { return samples[index].TTJD }
wherePlanet := func(xt, zn int) truncationCase {
return scalarTruncation(
fmt.Sprintf("planet.WherePlanetN(%s, zn=%d)", planetTruncationName[xt], zn),
jdOf,
func(jd float64, n int) float64 { return planet.WherePlanetN(xt, zn, jd, n) },
zn != 2,
planetTruncationTolerance[zn],
)
}
cases := make([]truncationCase, 0, 27)
for xt := 0; xt <= 7; xt++ {
for zn := 0; zn <= 2; zn++ {
cases = append(cases, wherePlanet(xt, zn))
}
}
return append(cases,
scalarTruncation("basic.HMoonTrueLoN", jdOf, basic.HMoonTrueLoN, true, []float64{5, 2.5, 1, 0.25}),
scalarTruncation("basic.HMoonTrueBoN", jdOf, basic.HMoonTrueBoN, true, []float64{3, 0.75, 0.15, 0.06}),
scalarTruncation("basic.HMoonAwayN", jdOf, basic.HMoonAwayN, false, []float64{40000, 7000, 2500, 400}),
)
}
func TestPlanetMoonTruncationConvergesToFullSeries(t *testing.T) {
samples := loadBaselineSamples(t)
assertTruncationConverges(t, planetMoonTruncationCases(samples), len(samples))
}
func publicSunMoonTruncationCases(t *testing.T, samples []baselineSample) []truncationCase {
t.Helper()
dates := baselineDates(t, samples)
dateOf := func(index int) time.Time { return dates[index] }
return []truncationCase{
dateTruncation("sun.TrueLoN", dateOf, sun.TrueLoN, []float64{0.15, 0.05, 0.02, 0.006}),
dateTruncation("sun.TrueBoN", dateOf, sun.TrueBoN, []float64{1e-3, 5e-4, 3e-4, 1.5e-4}),
dateTruncation("moon.TrueLoN", dateOf, moon.TrueLoN, []float64{5, 2.5, 1, 0.25}),
dateTruncation("moon.TrueBoN", dateOf, moon.TrueBoN, []float64{3, 0.75, 0.15, 0.06}),
}
}
func TestPublicSunMoonTruncationConvergesToFullSeries(t *testing.T) {
samples := loadBaselineSamples(t)
assertTruncationConverges(t, publicSunMoonTruncationCases(t, samples), len(samples))
}
-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(GetAsaTime(jde, 115, 32, 8, -6)))
fmt.Println("当前晨影 -12:", JDE2Date(GetAsaTime(jde, 115, 32, 8, -12)))
fmt.Println("当前昏影 -6:", JDE2Date(GetBanTime(jde, 115, 32, 8, -6)))
fmt.Println("当前昏影 -12:", JDE2Date(GetBanTime(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(GetMoonDownTime(jde, 115, 32, 8, 1, 10)))
fmt.Println("月相:", MoonLight(jde-8.0/24.0))
}
+50
View File
@@ -0,0 +1,50 @@
package basic
import (
"math"
"testing"
"time"
)
const ancientStationTolerance = 10.0 / 1440.0
func ancientStationTT(year int, month time.Month, day int) float64 {
return UTC2TT(Date2JD(time.Date(year, month, day, 0, 0, 0, 0, time.UTC)))
}
func ancientStationUT(year int, month time.Month, day, hour, minute int) float64 {
return Date2JD(time.Date(year, month, day, hour, minute, 0, 0, time.UTC))
}
func assertAncientNextStation(t *testing.T, queryTT, gotUT, wantUT float64) {
t.Helper()
if !eventUTQueryAfterOrEqual(gotUT, queryTT) {
t.Fatalf("station is before query: query TT %.9f, got UT %.9f", queryTT, gotUT)
}
if diff := math.Abs(gotUT - wantUT); diff > ancientStationTolerance {
t.Fatalf("station differs by %.3f minutes: got %s, want %s", diff*1440,
JD2DateByZone(gotUT, time.UTC, false), JD2DateByZone(wantUT, time.UTC, false))
}
}
func TestMarsAncientNextStationsStayOnTheirOppositionSides(t *testing.T) {
queryTT := ancientStationTT(-210, time.April, 1)
p2r := NextMarsProgradeToRetrograde(queryTT)
r2p := NextMarsRetrogradeToPrograde(queryTT)
assertAncientNextStation(t, queryTT, p2r, ancientStationUT(-209, time.March, 25, 20, 20))
assertAncientNextStation(t, queryTT, r2p, ancientStationUT(-209, time.June, 6, 11, 4))
if sameEventJD(p2r, r2p) {
t.Fatalf("typed Mars stations collapsed to one event: %.9f", p2r)
}
}
func TestMercuryAncientNextStationsDoNotSkipOrLoop(t *testing.T) {
janQueryTT := ancientStationTT(-210, time.January, 1)
assertAncientNextStation(t, janQueryTT, NextMercuryProgradeToRetrograde(janQueryTT), ancientStationUT(-210, time.February, 6, 21, 29))
assertAncientNextStation(t, janQueryTT, NextMercuryRetrogradeToPrograde(janQueryTT), ancientStationUT(-210, time.March, 1, 14, 20))
marQueryTT := ancientStationTT(-210, time.March, 1)
assertAncientNextStation(t, marQueryTT, NextMercuryProgradeToRetrograde(marQueryTT), ancientStationUT(-210, time.June, 11, 18, 50))
assertAncientNextStation(t, marQueryTT, NextMercuryRetrogradeToPrograde(marQueryTT), ancientStationUT(-210, time.March, 1, 14, 20))
}
+6
View File
@@ -0,0 +1,6 @@
package basic
import "math"
const rad = math.Pi / 180.0
const deg = 180.0 / math.Pi
+265
View File
@@ -0,0 +1,265 @@
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 {
// JD 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JD 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{
JD: TT2UTC(eventTT),
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 := UTC2TT(Date2JD(startUTC))
endTT := UTC2TT(Date2JD(endUTC))
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 := TT2UTC(eventTT)
eventTimeUTC := JD2DateByZone(eventUT, time.UTC, false)
if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) {
continue
}
events = append(events, ApsisEvent{
JD: eventUT,
Distance: distanceKM,
})
}
sort.Slice(events, func(i, j int) bool {
return events[i].JD < events[j].JD
})
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) {
if !finite(seed) || !finite(cfg.bracketHalfWidth) || !finite(cfg.sampleStep) || cfg.sampleStep <= 0 {
return seed, math.NaN()
}
best := seed
bestDistance := distanceFn(seed)
// seed 幅度很大时 sample += sampleStep 可能小于一个 ULP(浮点间隔)而永不推进,
// 因此显式检测步长是否真的改变了采样点,避免无界循环。
// At a large |seed| the step can fall below one ULP, so `sample += step` would never
// advance; detect a step that does not move the sample and stop the sweep instead.
for sample, end := seed-cfg.bracketHalfWidth, seed+cfg.bracketHalfWidth+1e-12; sample <= end; {
dist := distanceFn(sample)
if distanceBetter(dist, bestDistance, cfg.maximize) {
best = sample
bestDistance = dist
}
next := sample + cfg.sampleStep
if next == sample {
break
}
sample = next
}
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 := JD2DateByZone(got.JD, 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 := JD2DateByZone(got.JD, 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)
}
+137
View File
@@ -0,0 +1,137 @@
package basic
import "math"
const (
// 线性时标关系的参考历元,不表示各时标读数在此相等。
barycentricT0JDE = 2443144.5003725
lgRate = 6.969290134e-10
lbRate = 1.550519768e-8
tdb0Seconds = -65.5e-6
)
// tdbPeriodicGroups 按 t 的幂次分组的 TDB−TT 周期项(振幅秒、频率 rad/儒略千年、相位 rad)。
var tdbPeriodicGroups = [5][][3]float64{
{
{0.001656674564, 6283.075849991, 6.240054195},
{2.2417471e-05, 5753.384884897, 4.296977442},
{1.3839792e-05, 12566.151699983, 6.19690441},
{4.770086e-06, 529.690965095, 0.444401603},
{4.67674e-06, 6069.776754553, 4.021195093},
{2.256707e-06, 213.299095438, 5.543113262},
{1.694205e-06, -3.523118349, 5.025132748},
{1.554905e-06, 77713.77146792, 5.19846709},
{1.276839e-06, 7860.419392439, 5.988822341},
{1.193379e-06, 5223.693919802, 3.64982373},
{1.115322e-06, 3930.20969622, 1.422745069},
{7.94185e-07, 11506.769769794, 2.322313077},
{4.47061e-07, 26.2983198, 3.615796498},
{4.35206e-07, -398.149003408, 4.349338347},
{6.00309e-07, 1577.343542448, 2.678271909},
{4.96817e-07, 6208.294251424, 5.696701824},
{4.86306e-07, 5884.926846583, 0.520007179},
{4.32392e-07, 74.781598567, 2.435898309},
{4.68597e-07, 6244.942814354, 5.866398759},
{3.7551e-07, 5507.553238667, 4.103476804},
{2.43085e-07, -775.522611324, 3.651837925},
{1.73435e-07, 18849.227549974, 6.153743485},
{2.30685e-07, 5856.477659115, 4.773852582},
{2.03747e-07, 12036.460734888, 4.333987818},
},
{
{0.000102156724, 6283.075849991, 4.249032005},
{1.706807e-06, 12566.151699983, 4.205904248},
{2.69668e-07, 213.299095438, 3.400290479},
{2.65919e-07, 529.690965095, 5.836047367},
{2.10568e-07, -3.523118349, 6.262738348},
{7.7996e-08, 5223.693919802, 4.670344204},
{5.4764e-08, 1577.343542448, 4.53480017},
{5.9146e-08, 26.2983198, 1.083044735},
},
{
{4.32299e-06, 6283.075849991, 2.642893748},
{4.06495e-07, 0.0, 4.71238898},
{1.22605e-07, 12566.151699983, 2.438140634},
{1.9476e-08, 213.299095438, 1.642186981},
},
{
{1.43388e-07, 6283.075849991, 1.131453581},
{6.671e-09, 12566.151699983, 0.775148887},
},
{
{3.826e-09, 6283.075849991, 5.705257275},
{3.03e-10, 12566.151699983, 5.407132842},
},
}
// 地心近似,不包含观测者位置引起的日周项。
func tdbPeriodicSeconds(jd float64) float64 {
t := (jd - 2451545.0) / 365250.0
sum := 0.0
for i := len(tdbPeriodicGroups) - 1; i >= 0; i-- {
group := 0.0
for _, term := range tdbPeriodicGroups[i] {
group += term[0] * math.Sin(term[1]*t+term[2])
}
sum = sum*t + group
}
return sum + 0.00065e-6*math.Sin(6069.776754*t+4.021194) +
0.00033e-6*math.Sin(213.299095*t+5.543132) -
0.00196e-6*math.Sin(6208.294251*t+5.696701) -
0.00173e-6*math.Sin(74.781599*t+2.435900) +
0.03638e-6*t*t
}
// TCGMinusTTSeconds 返回 TT 时刻的 TCG−TT(秒)/ TCG−TT in seconds at a TT instant.
func TCGMinusTTSeconds(jd float64) float64 {
return lgRate / (1 - lgRate) * (jd - barycentricT0JDE) * 86400
}
// TCBMinusTTSeconds 返回 TT 时刻的地心 TCB−TT 近似值(秒)/ geocentric TCB−TT approximation in seconds at a TT instant.
func TCBMinusTTSeconds(jd float64) float64 {
return (lbRate*(jd-barycentricT0JDE)*86400 + tdbPeriodicSeconds(jd) - tdb0Seconds) / (1 - lbRate)
}
// TDBMinusTTSeconds 返回 TT 时刻的地心 TDB−TT 近似值(秒)/ geocentric TDB−TT approximation in seconds at a TT instant.
func TDBMinusTTSeconds(jd float64) float64 { return tdbPeriodicSeconds(jd) }
// TT2TCG 地球时转地心坐标时 / converts TT to TCG.
func TT2TCG(ttJDE float64) float64 { return ttJDE + TCGMinusTTSeconds(ttJDE)/86400 }
// TCG2TT 地心坐标时转地球时 / converts TCG to TT.
func TCG2TT(tcgJDE float64) float64 { return tcgJDE - lgRate*(tcgJDE-barycentricT0JDE) }
// TT2TCB 地球时转太阳系质心坐标时,采用地心近似 / converts TT to TCB using a geocentric approximation.
func TT2TCB(ttJDE float64) float64 { return ttJDE + TCBMinusTTSeconds(ttJDE)/86400 }
// TCB2TT 是 TT2TCB 的逆 / inverts TT2TCB.
func TCB2TT(tcbJDE float64) float64 {
// TDB 与 TT 相差不超过 2 ms,拿 TDB 读数当初值可少迭代两轮。
tt := TCB2TDB(tcbJDE)
for i := 0; i < 3; i++ {
tt = tcbJDE - TCBMinusTTSeconds(tt)/86400
}
return tt
}
// TT2TDB 地球时转太阳系质心力学时,采用地心近似 / converts TT to TDB using a geocentric approximation.
func TT2TDB(ttJDE float64) float64 { return ttJDE + TDBMinusTTSeconds(ttJDE)/86400 }
// TDB2TT 是 TT2TDB 的逆 / inverts TT2TDB.
func TDB2TT(tdbJDE float64) float64 {
tt := tdbJDE - TDBMinusTTSeconds(tdbJDE)/86400
for i := 0; i < 3; i++ {
tt = tdbJDE - TDBMinusTTSeconds(tt)/86400
}
return tt
}
// TCB2TDB 太阳系质心坐标时转质心力学时 / converts TCB to TDB.
func TCB2TDB(tcbJDE float64) float64 {
return tcbJDE + (tdb0Seconds/86400 - lbRate*(tcbJDE-barycentricT0JDE))
}
// TDB2TCB 太阳系质心力学时转质心坐标时 / converts TDB to TCB.
func TDB2TCB(tdbJDE float64) float64 {
return tdbJDE + (lbRate*(tdbJDE-barycentricT0JDE)-tdb0Seconds/86400)/(1-lbRate)
}
+128
View File
@@ -0,0 +1,128 @@
package basic
import (
"math"
"testing"
)
func TestBarycentricScaleDefinitions(t *testing.T) {
for _, jd := range []float64{JDCalc(-3000, 1, 1), barycentricT0JDE, JDCalc(2026, 1, 1), JDCalc(6000, 1, 1)} {
tcgOffset := TCGMinusTTSeconds(jd)
if diff := tcgOffset*(1-6.969290134e-10) - 6.969290134e-10*(jd-2443144.5003725)*86400; math.Abs(diff) > 1e-12 {
t.Errorf("jd=%v TCG defining relation residual=%g s", jd, diff)
}
tcbOffset := TCBMinusTTSeconds(jd)
if diff := tcbOffset*(1-1.550519768e-8) - 1.550519768e-8*(jd-2443144.5003725)*86400 - TDBMinusTTSeconds(jd) - 65.5e-6; math.Abs(diff) > 1e-12 {
t.Errorf("jd=%v TCB defining relation residual=%g s", jd, diff)
}
}
if got := TCGMinusTTSeconds(barycentricT0JDE); got != 0 {
t.Errorf("TCG-TT at T0=%v, want 0", got)
}
if got, want := TCB2TDB(barycentricT0JDE), barycentricT0JDE-65.5e-6/86400; got != want {
t.Errorf("TDB at TCB T0=%.12f, want %.12f", got, want)
}
}
func TestBarycentricLinearConversionAnchors(t *testing.T) {
for _, tc := range []struct {
name string
fn func(float64) float64
jd float64
want float64
}{
{"TCB2TDB", TCB2TDB, 2453750.5 + 0.893019599, 2453750.5 + 0.8928551362746343397},
{"TDB2TCB", TDB2TCB, 2453750.5 + 0.892855137, 2453750.5 + 0.8930195997253656716},
{"TCG2TT", TCG2TT, 2453750.5 + 0.892862531, 2453750.5 + 0.8928551387488816828},
{"TT2TCG", TT2TCG, 2453750.5 + 0.892482639, 2453750.5 + 0.8924900312508587113},
} {
ulp := math.Nextafter(tc.want, math.Inf(1)) - tc.want
if got := tc.fn(tc.jd); math.Abs(got-tc.want) > ulp {
t.Errorf("%s=%.12f, want %.12f", tc.name, got, tc.want)
}
}
}
func TestBarycentricPeriodicTermsMatchFullSeries(t *testing.T) {
for _, tc := range []struct {
jd float64
want float64
}{
{625307.5, 0.001558488628712037},
{766574.5, -0.000924800103328196},
{1721057.5, 0.0008624527814561311},
{2433282.5, -7.069829559472634e-05},
{2443144.5, -6.551401857064118e-05},
{2451544.5, -0.0001137630988927298},
{2461041.5, -8.20152430051247e-05},
{2461222.5, 0.0001186656922342686},
{2469807.5, -8.01882947792431e-05},
{3182029.5, -0.0009618304620565844},
{3912514.5, -0.00138271667055309},
} {
if got := TDBMinusTTSeconds(tc.jd); math.Abs(got-tc.want) > 2.1e-6 {
t.Errorf("jd=%v TDB-TT=%.9f us, want %.9f us", tc.jd, got*1e6, tc.want*1e6)
}
}
}
func TestBarycentricScaleRoundTrips(t *testing.T) {
for _, jd := range []float64{
JDCalc(-3000, 1, 1), JDCalc(0, 1, 1), JDCalc(1900, 1, 1), JDCalc(1950, 6, 1),
barycentricT0JDE, JDCalc(2000, 1, 1), JDCalc(2026, 4, 1), JDCalc(2050, 7, 1),
JDCalc(2100, 1, 1), JDCalc(6000, 1, 1),
} {
ulp := math.Nextafter(jd, math.Inf(1)) - jd
for _, tc := range []struct {
name string
fwd func(float64) float64
back func(float64) float64
}{
{"TT-TCG", TT2TCG, TCG2TT},
{"TT-TCB", TT2TCB, TCB2TT},
{"TT-TDB", TT2TDB, TDB2TT},
{"TCB-TDB", TCB2TDB, TDB2TCB},
} {
if got := tc.back(tc.fwd(jd)); math.Abs(got-jd) > ulp {
t.Errorf("jd=%v %s round-trip error=%g s", jd, tc.name, (got-jd)*86400)
}
if got := tc.fwd(tc.back(jd)); math.Abs(got-jd) > ulp {
t.Errorf("jd=%v %s inverse round-trip error=%g s", jd, tc.name, (got-jd)*86400)
}
}
// 组合路径分别舍入,允许两次 JD 舍入误差。
if diff := TCB2TDB(TT2TCB(jd)) - TT2TDB(jd); math.Abs(diff) > 2*ulp {
t.Errorf("jd=%v TT-TCB-TDB differs from TT-TDB by %g s", jd, diff*86400)
}
if diff := (TT2TDB(jd)-jd)*86400 - TDBMinusTTSeconds(jd); math.Abs(diff) > ulp*86400 {
t.Errorf("jd=%v TDB seconds and JD conversions differ by %g s", jd, diff)
}
}
}
func TestBarycentricScaleMagnitudes(t *testing.T) {
jd := JDCalc(2026, 1, 1)
if got := TCGMinusTTSeconds(jd); math.Abs(got-1.0777) > 5e-3 {
t.Errorf("TCG-TT=%v s, want about 1.078", got)
}
if got := TCBMinusTTSeconds(jd); math.Abs(got-23.9757) > 5e-3 {
t.Errorf("TCB-TT=%v s, want about 23.976", got)
}
if got := TCGMinusTTSeconds(barycentricT0JDE+365.25) - TCGMinusTTSeconds(barycentricT0JDE); math.Abs(got-0.02204) > 5e-5 {
t.Errorf("TCG-TT annual increase=%v s, want about 0.02204", got)
}
if got := TCBMinusTTSeconds(barycentricT0JDE+365.25) - TCBMinusTTSeconds(barycentricT0JDE); math.Abs(got-0.48934) > 5e-4 {
t.Errorf("TCB-TT annual increase=%v s, want about 0.48934", got)
}
if got := TDBMinusTTSeconds(JDCalc(2100, 1, 1)) - TDBMinusTTSeconds(JDCalc(2000, 1, 1)); math.Abs(got) > 2e-4 {
t.Errorf("TDB-TT century difference=%v s, want within 0.2 ms", got)
}
min, max := math.Inf(1), math.Inf(-1)
for month := 1; month <= 12; month++ {
value := TDBMinusTTSeconds(JDCalc(2026, month, 1))
min, max = math.Min(min, value), math.Max(max, value)
}
if amplitude := max - min; amplitude < 3.0e-3 || amplitude > 3.6e-3 {
t.Errorf("TDB-TT annual range=%v s, want 3.0-3.6 ms", amplitude)
}
}
-24
View File
@@ -1,24 +0,0 @@
package basic
import (
"fmt"
"testing"
"time"
)
func Test_JDECALC(t *testing.T) {
i := 10
for i > 0 {
fmt.Printf("%.18f\n", GetNowJDE())
time.Sleep(time.Second)
i--
}
}
func Test_TDUT(t *testing.T) {
fmt.Printf("%.18f\n", DeltaT(2119.5, false))
}
func Test_JDE2(t *testing.T) {
fmt.Println(JDE2Date(2458868.500))
}
-438
View File
@@ -1,438 +0,0 @@
package basic
import (
"fmt"
"math"
"strings"
"time"
)
var defDeltaTFn = DefaultDeltaT
/*
@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 dt_ext(y, jsd float64) float64 { // 二次曲线外推,用于数值外插
dy := (y - 1820) / 100.00
return -20 + jsd*dy*dy
}
func dt_cal(y float64) float64 { //传入年, 返回世界时UT与原子时(力学时 TD)之差, ΔT = TD - UT
dt_at := []float64{
-4000, 108371.7, -13036.80, 392.000, 0.0000,
-500, 17201.0, -627.82, 16.170, -0.3413,
-150, 12200.6, -346.41, 5.403, -0.1593,
150, 9113.8, -328.13, -1.647, 0.0377,
500, 5707.5, -391.41, 0.915, 0.3145,
900, 2203.4, -283.45, 13.034, -0.1778,
1300, 490.1, -57.35, 2.085, -0.0072,
1600, 120.0, -9.81, -1.532, 0.1403,
1700, 10.2, -0.91, 0.510, -0.0370,
1800, 13.4, -0.72, 0.202, -0.0193,
1830, 7.8, -1.81, 0.416, -0.0247,
1860, 8.3, -0.13, -0.406, 0.0292,
1880, -5.4, 0.32, -0.183, 0.0173,
1900, -2.3, 2.06, 0.169, -0.0135,
1920, 21.2, 1.69, -0.304, 0.0167,
1940, 24.2, 1.22, -0.064, 0.0031,
1960, 33.2, 0.51, 0.231, -0.0109,
1980, 51.0, 1.29, -0.026, 0.0032,
2000, 63.87, 0.1, 0, 0,
2005, 64.7, 0.4, 0, 0, //一次项记为x,则 10x=0.4秒/年*(2015-2005),解得x=0.4
2015, 69,
}
y0 := dt_at[len(dt_at)-2] //表中最后一年
t0 := dt_at[len(dt_at)-1] //表中最后一年的 deltatT
if y >= y0 {
jsd := float64(31) // sjd是y1年之后的加速度估计
// 瑞士星历表jsd=31, NASA网站jsd=32, skmap的jsd=29
if y > y0+100.00 {
return dt_ext(y, jsd)
}
v := dt_ext(y, jsd) //二次曲线外推
dv := dt_ext(y0, jsd) - t0 // ye年的二次外推与te的差
return (v - dv*(y0+100.00-y)/100.00)
}
d := dt_at
var i int
for i = 0; i < len(d); i += 5 {
if float64(y) < d[i+5] {
break
// 判断年所在的区间
}
}
t1 := (y - d[i]) / (d[i+5] - d[i]) * 10.00 //////// 三次插值, 保证精确性
t2 := t1 * t1
t3 := t2 * t1
res := d[i+1] + d[i+2]*t1 + d[i+3]*t2 + d[i+4]*t3
return (res)
}
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 OldDefaultDeltaT(Date float64, IsJDE bool) (Result float64) { //传入年或儒略日,传出为秒
var Year float64
if IsJDE {
dates := JDE2Date(Date)
Year = float64(dates.Year()) + float64(dates.YearDay())/365.0
} else {
Year = Date
}
if Year < 2100 && Year >= 2010 {
return dt_cal(Year)
}
return DefaultDeltaT(Date, IsJDE)
}
func DefaultDeltaT(Date float64, IsJDE bool) (Result float64) { //传入年或儒略日,传出为秒
var Year float64
if IsJDE {
dates := JDE2Date(Date)
Year = float64(dates.Year()) + float64(dates.YearDay())/365.0
} else {
Year = Date
}
if Year < 2010 {
Result = dt_cal(Year)
return
}
if Year < 2100 && Year >= 2010 {
var t = (Year - 2000.0)
Result = 62.92 + 0.32217*t + 0.005589*t*t
return
}
if Year >= 2100 && Year <= 2150 {
Result = -20 + 32*(((Year-1820.0)/100.0)*((Year-1820.0)/100.0)) - 0.5628*(2150-Year)
return
}
if Year > 2150 {
//tmp=(Year-1820)/100;
//Result= -20 + 32 * tmp*tmp;
Result = dt_cal(Year)
return
}
return
}
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
}
}
/*
* @name: JDE转日期,输出为数组
*/
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) (lmonth, lday int, leap bool, result string) {
jde := JDECalc(year, month, float64(day)) //计算当前JDE时间
if month == 11 || month == 12 { //判断当前日期属于前一年周期还是后一年周期
//判断方法:当前日期与冬至日所在朔望月的关系
winterday := GetJQTime(year, 270) + tz //冬至日日期(世界时,北京时间)
Fday := TD2UT(CalcMoonS(float64(year)+11.0/12.0+5.0/30.0/12.0, 0), true) + tz //朔月(世界时,北京时间)
Yday := TD2UT(CalcMoonS(float64(year)+1.0, 0), true) + tz //下一朔月(世界时,北京时间)
if Fday-math.Floor(Fday) > 0.5 {
Fday = math.Floor(Fday) + 0.5
} else {
Fday = math.Floor(Fday) - 0.5
}
if Yday-math.Floor(Yday) > 0.5 {
Yday = math.Floor(Yday) + 0.5
} else {
Yday = math.Floor(Yday) - 0.5
}
if winterday >= Fday && winterday < Yday && jde <= Fday {
year--
}
if winterday >= Yday && jde < Yday {
year--
}
} else {
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
}
i := 0
for i = min - 1; i <= max; i++ {
if moon[i] > jde {
break
}
}
lmonth = i - min
var sleap bool = false
leap = false
if lmonth >= leapmonth {
sleap = true
}
if lmonth == leapmonth {
leap = true
}
if lmonth < 2 {
lmonth += 11
} else {
lmonth--
}
if sleap {
lmonth--
}
if lmonth <= 0 {
lmonth += 12
}
lday = int(jde-moon[i-1]) + 1
strmonth := []string{"十", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊"}
strday := []string{"初", "十", "廿", "三"}
if leap {
result += "闰"
}
if lmonth == 1 {
result += "正月"
} else {
result += strmonth[lmonth] + "月"
}
if lday == 20 {
result += "二十"
} else if lday == 10 {
result += "初十"
} else {
result += strday[lday/10] + strmonth[lday%10]
}
return
}
func GetSolar(year, month, day int, leap bool, tz float64) float64 {
if month < 11 {
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
}
if leap {
month++
}
if month > 10 {
month -= 11
} else {
month++
}
if month >= leapmonth && !leap {
month++
}
jde := moon[min-1+month] + float64(day) - 1
return jde
}
// 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)
}
+53 -117
View File
@@ -1,134 +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++
}
func TestGetJQTime(t *testing.T) {
originalFunc := func(year, angle int) float64 {
const iterations = 1
var day int
if angle%2 == 0 {
day = 18
} else {
day = 3
}
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]) {
month := 3.0
if angle%10 != 0 {
month += float64(angle+15) / 30.0
} else {
month += float64(angle) / 30.0
}
if month > 12 {
month -= 12
}
jd := JDCalc(year, int(month), float64(day))
if angle == 0 {
angle = 360
}
for i := 0; i < iterations; i++ {
for {
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
}
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])})
}
jd -= 0.001
}
jd += 0.001
return TT2UTC(jd)
}
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
testCases := []struct {
year int
angle int
}{
{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},
{2200, 0}, {2200, 30}, {2200, 90}, {2200, 180}, {2200, 270},
}
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
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.Fatalf("GetJQTime mismatch: year=%d angle=%d original=%.15f optimized=%.15f diff=%.15f",
tc.year, tc.angle, originalResult, optimizedResult, diff)
}
}
}
+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 := Date2JD(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")
}
}
+24
View File
@@ -0,0 +1,24 @@
package basic
import (
"testing"
)
func TestConstellationNameZH(t *testing.T) {
now := GetNowJD()
//finish on 30s
for i := 0.00; i <= 360.00; i += 0.5 {
for j := -90.00; j <= 90.00; j += 0.5 {
ConstellationNameZH(float64(i), float64(j), now)
}
}
}
func BenchmarkConstellationNameZH(b *testing.B) {
jde := GetNowJD()
for i := 0; i < b.N; i++ {
//GetNowJD()
ConstellationNameZH(11.11, 12.12, jde)
}
}
+51 -75
View File
@@ -10,7 +10,8 @@ import (
* 坐标变换,黄道转赤道
*/
func LoToRa(jde, lo, bo float64) float64 {
ra := math.Atan2(Sin(lo)*Cos(Sita(jde))-Tan(bo)*Sin(Sita(jde)), Cos(lo))
eps := TrueObliquity(jde)
ra := math.Atan2(Sin(lo)*Cos(eps)-Tan(bo)*Sin(eps), Cos(lo))
ra = ra * 180 / math.Pi
if ra < 0 {
ra += 360
@@ -19,13 +20,17 @@ 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))
eps := TrueObliquity(jde)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*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))
eps := TrueObliquity(jde)
sinEps := Sin(eps)
cosEps := Cos(eps)
dec := ArcSin(Sin(bo)*cosEps + Cos(bo)*sinEps*Sin(lo))
ra := math.Atan2(Sin(lo)*cosEps-Tan(bo)*sinEps, Cos(lo))
ra = ra * 180 / math.Pi
if ra < 0 {
ra += 360
@@ -36,9 +41,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 +51,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,32 +60,11 @@ 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)
}
/*
* 赤道坐标岁差变换st end 为JDE时刻
*/
func ZuoBiaoSuiCha(ra, dec, st, end float64) (float64, float64) {
t := (end - st) / 36525.0
l := (2306.2181*t + 0.30188*t*t + 0.017998*t*t*t) / 3600
z := (2306.2181*t + 1.09468*t*t + 0.018203*t*t*t) / 3600
o := (2004.3109*t - 0.42665*t*t + 0.041833*t*t*t) / 3600
A := Cos(dec) * Sin(ra+l)
B := Cos(o)*Cos(dec)*Cos(ra+l) - Sin(o)*Sin(dec)
C := Sin(o)*Cos(dec)*Cos(ra+l) + Cos(o)*Sin(dec)
ras := math.Atan2(A, B)
ras = ras * 180 / math.Pi
if ras < 0 {
ras += 360
}
ra = ras + z
dec = ArcSin(C)
return ra, dec
}
/*
* 地心坐标转站心坐标,参数分别为,地心赤经赤纬 纬度经度,jde,离地心位置au
*/
@@ -101,73 +85,65 @@ 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) {
return topocentricRaDecWithSidereal(ra, dec, lat, lon, ApparentSiderealTime(UTC2UT1(jd))*15, au, h)
}
func topocentricRaDecWithSidereal(ra, dec, lat, lon, siderealDegrees, au, h float64) (float64, float64) {
sinpi := Sin(0.0024427777777) / au
pcosi := pcosi(lat, h)
psini := psini(lat, h)
tH := Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + lon - ra)
tH := Limit360(siderealDegrees + lon - ra)
nra := math.Atan2(-pcosi*sinpi*Sin(tH), (Cos(dec)-pcosi*sinpi*Cos(tH))) * 180 / math.Pi
ndec := math.Atan2((Sin(dec)-psini*sinpi)*Cos(nra), (Cos(dec)-pcosi*sinpi*Cos(tH))) * 180 / math.Pi
return ra + nra, ndec
}
func ZhanXinRa(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 TopocentricRa(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
topocentricRA, _ := TopocentricRaDec(ra, dec, lat, lon, jd, au, h)
return topocentricRA
}
func ZhanXinDec(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
sinpi := Sin(0.0024427777777) / au
pcosi := pcosi(lat, h)
psini := psini(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
ndec := math.Atan2((Sin(dec)-psini*sinpi)*Cos(nra), (Cos(dec)-pcosi*sinpi*Cos(tH))) * 180 / math.Pi
return ndec
func TopocentricDec(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时
_, topocentricDec := TopocentricRaDec(ra, dec, lat, lon, jd, au, h)
return topocentricDec
}
func ZhanXinLo(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
// TopocentricLoBo 一次求值给出站心黄经与黄纬 / topocentric ecliptic longitude and latitude in one solve.
//
// 先解站心赤道坐标再转黄道:黄道版公式的分母在黄经 90°–270° 时变号,直接 atan2 会切到对顶象限。
func TopocentricLoBo(lo, bo, lat, lon, jde, au, h float64) (float64, float64) {
ra, dec := LoBoToRaDec(jde, lo, bo)
topRA, topDec := TopocentricRaDec(ra, dec, lat, lon, jde, au, h)
return RaDecToLoBo(jde, topRA, topDec)
}
// TopocentricLo 站心黄经 / topocentric ecliptic longitude.
func TopocentricLo(lo, bo, lat, lon, jde, au, h float64) float64 {
nlo, _ := TopocentricLoBo(lo, bo, lat, lon, jde, au, h)
return nlo
}
func ZhanXinBo(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
// TopocentricBo 站心黄纬 / topocentric ecliptic latitude.
func TopocentricBo(lo, bo, lat, lon, jde, au, h float64) float64 {
_, nbo := TopocentricLoBo(lo, bo, lat, lon, jde, au, h)
return nbo
}
func GXCLo(lo, bo, jd float64) float64 { //光行差修正
func GXCLo(lo, bo, jde float64) float64 { //光行差修正
k := 20.49552
sunlo := SunTrueLo(jd)
e := Earthe(jd)
epi := EarthPI(jd)
sunlo := SunTrueLo(jde)
e := Earthe(jde)
epi := EarthPI(jde)
tmp := (-k*Cos(sunlo-lo) + e*k*Cos(epi-lo)) / Cos(bo)
return tmp
}
func GXCBo(lo, bo, jd float64) float64 {
func GXCBo(lo, bo, jde float64) float64 {
k := 20.49552
sunlo := SunTrueLo(jd)
e := Earthe(jd)
epi := EarthPI(jd)
sunlo := SunTrueLo(jde)
e := Earthe(jde)
epi := EarthPI(jde)
tmp := -k * Sin(bo) * (Sin(sunlo-lo) - e*Sin(epi-lo))
return tmp
}
+44
View File
@@ -0,0 +1,44 @@
package basic
import (
"math"
"testing"
. "b612.me/astro/tools"
)
func TestEclipticToEquatorialCachedObliquityMatchesRepeatedEvaluation(t *testing.T) {
for _, test := range []struct {
jde float64
lo float64
bo float64
}{
{jde: 2451545.0, lo: 0, bo: 0},
{jde: 2460832.0, lo: 193.25, bo: -5.75},
{jde: 2378496.5, lo: 359.999, bo: 87.5},
} {
legacyDec := ArcSin(
Sin(test.bo)*Cos(TrueObliquity(test.jde)) +
Cos(test.bo)*Sin(TrueObliquity(test.jde))*Sin(test.lo),
)
legacyRA := math.Atan2(
Sin(test.lo)*Cos(TrueObliquity(test.jde))-Tan(test.bo)*Sin(TrueObliquity(test.jde)),
Cos(test.lo),
) * 180 / math.Pi
if legacyRA < 0 {
legacyRA += 360
}
ra, dec := LoBoToRaDec(test.jde, test.lo, test.bo)
if ra != legacyRA || dec != legacyDec {
t.Fatalf("LoBoToRaDec(%v, %v, %v) = %.15g %.15g, want %.15g %.15g",
test.jde, test.lo, test.bo, ra, dec, legacyRA, legacyDec)
}
if got := LoToRa(test.jde, test.lo, test.bo); got != legacyRA {
t.Fatalf("LoToRa(%v, %v, %v) = %.15g, want %.15g", test.jde, test.lo, test.bo, got, legacyRA)
}
if got := BoToDec(test.jde, test.lo, test.bo); got != legacyDec {
t.Fatalf("BoToDec(%v, %v, %v) = %.15g, want %.15g", test.jde, test.lo, test.bo, got, legacyDec)
}
}
}
-50
View File
@@ -1,50 +0,0 @@
package basic
import (
"fmt"
"testing"
)
func Test_LoBo(t *testing.T) {
fmt.Printf("%.9f", dt_cal(2020.5))
}
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))
}
+82
View File
@@ -0,0 +1,82 @@
package basic
import (
"math"
"testing"
"time"
. "b612.me/astro/tools"
)
func TestTopocentricRaDecUsesUTJulianDateForSiderealTime(t *testing.T) {
ut := Date2JD(time.Date(2025, 6, 5, 12, 2, 7, 700000000, time.UTC))
ra := 189.527817246
dec := -5.973400893
lat := 6.79657
lon := 121.55381
distanceAU := HMoonAwayN(UTC2TT(ut), -1) / 149597870.7
gotRA, gotDec := TopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0)
wantRA, wantDec := independentTopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0)
// 逐分量比较:零距离处 acos 度规的病态下限会把 1 ulp 放大成毫角秒。
if deltaRA, deltaDec := math.Abs(gotRA-wantRA), math.Abs(gotDec-wantDec); deltaRA > 1e-12 || deltaDec > 1e-12 {
t.Fatalf("TopocentricRaDec differs from independent formula: dRA=%.3g dDec=%.3g deg", deltaRA, deltaDec)
}
}
func TestTopocentricRaAndDecMatchCombinedResult(t *testing.T) {
ut := Date2JD(time.Date(2025, 6, 5, 12, 2, 7, 700000000, time.UTC))
ra := 189.527817246
dec := -5.973400893
lat := 6.79657
lon := 121.55381
distanceAU := HMoonAwayN(UTC2TT(ut), -1) / 149597870.7
wantRA, wantDec := TopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0)
if got := TopocentricRa(ra, dec, lat, lon, ut, distanceAU, 0); got != wantRA {
t.Fatalf("TopocentricRa = %.12f, want %.12f", got, wantRA)
}
if got := TopocentricDec(ra, dec, lat, lon, ut, distanceAU, 0); got != wantDec {
t.Fatalf("TopocentricDec = %.12f, want %.12f", got, wantDec)
}
}
func TestHMoonHeightUsesUTForTopocentricCorrection(t *testing.T) {
ut := Date2JD(time.Date(2026, 4, 28, 16, 1, 30, 0, time.UTC))
longitude := 0.0
latitude := 51.4779
ra, dec := HMoonApparentRaDecN(ut, longitude, latitude, 0, -1)
hourAngle := Limit360(ApparentSiderealTime(UTC2UT1(ut))*15 + longitude - ra)
want := ArcSin(Sin(latitude)*Sin(dec) + Cos(dec)*Cos(latitude)*Cos(hourAngle))
got := HMoonHeightN(ut, longitude, latitude, 0, -1)
if difference := math.Abs(got - want); difference > 1e-10 {
t.Fatalf("HMoonHeightN differs from the UT topocentric position by %.12f degrees", difference)
}
}
func independentTopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, height float64) (float64, float64) {
const (
equatorialRadiusKM = 6378.14
polarRadiusKM = 6356.755
)
u := math.Atan(polarRadiusKM / equatorialRadiusKM * Tan(lat))
rhoCos := math.Cos(u) + height/6378140.0*Cos(lat)
rhoSin := polarRadiusKM/equatorialRadiusKM*math.Sin(u) + height/6378140.0*Sin(lat)
sinParallax := Sin(0.0024427777777) / distanceAU
hourAngle := Limit360(ApparentSiderealTime(UTC2UT1(ut))*15 + lon - ra)
deltaRA := math.Atan2(
-rhoCos*sinParallax*Sin(hourAngle),
Cos(dec)-rhoCos*sinParallax*Cos(hourAngle),
)
topRA := ra + deltaRA*180/math.Pi
topDec := math.Atan2(
(Sin(dec)-rhoSin*sinParallax)*math.Cos(deltaRA),
Cos(dec)-rhoCos*sinParallax*Cos(hourAngle),
) * 180 / math.Pi
return topRA, topDec
}
func angularDistanceArcsec(ra1, dec1, ra2, dec2 float64) float64 {
cosDistance := Sin(dec1)*Sin(dec2) + Cos(dec1)*Cos(dec2)*Cos(ra1-ra2)
return math.Acos(math.Max(-1, math.Min(1, cosDistance))) * 180 / math.Pi * 3600
}
-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 = ZuoBiaoSuiCha(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]
}
-24
View File
@@ -1,24 +0,0 @@
package basic
import (
"testing"
)
func Test_Isxz(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)
}
}
}
func Benchmark_IsXZ(b *testing.B) {
jde := GetNowJDE()
for i := 0; i < b.N; i++ {
//GetNowJDE()
WhichCst(11.11, 12.12, jde)
}
}
+95
View File
@@ -0,0 +1,95 @@
package basic
import (
"math"
"testing"
"time"
)
// 本文件核实太阳中天(floor 正午锚点)与月亮中天(floor+0.5 午夜锚点)两套锚点:
// 调用方按各自约定补偿后,两者都必须落在同一个本地民用日,并接近当日本地日的真实中天。
//
// 框架约定(由各自调用方固定下来):
// - SunHeight/HMoonHeight 的 jd 是本地民用时框架,本地民用日从 Date2JD(本地 0 时) 起算;
// - basic.CulminationTime 的返回值是本地民用时框架,sun/sun.go 减 tz/24 得到 UT;
// - basic.MoonCulminationTime 的返回值就是本地民用时框架,moon/moon.go 按 byZone=true 使用。
type culminationAnchorSite struct {
name string
lon float64
lat float64
tz float64
}
var culminationAnchorSites = []culminationAnchorSite{
{"beijing", 116.4074, 39.9042, 8},
{"utc", 0, 0, 0},
{"newyork", -74, 40.7, -5},
{"adelaide", 138.6, -34.9, 9.5},
{"chatham", -176.5, -43.9, 12.75},
{"kiritimati", -157.4, 1.9, 14},
}
func culminationAnchorLocalMidnight(site culminationAnchorSite, timestamp time.Time) (time.Time, float64) {
location := time.FixedZone("anchor", int(site.tz*3600))
local := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), 0, 0, 0, 0, location)
return local, Date2JD(local)
}
func TestSunCulminationAnchorKeepsLocalDay(t *testing.T) {
timestamp := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
for _, site := range culminationAnchorSites {
local, midnightJD := culminationAnchorLocalMidnight(site, timestamp)
location := local.Location()
// sun/sun.go 的补偿:本地 0 时 JD 再加半天,使 floor 落在同一本地日;随后减 tz/24 得 UT。
got := JD2DateByZone(CulminationTime(midnightJD+0.5, site.lon, site.tz)-site.tz/24, location, false)
truthJD, truthAltitude := 0.0, -999.0
for minute := 0; minute <= 24*60; minute++ {
jd := midnightJD + float64(minute)/1440.0
if altitude := SunHeight(jd, site.lon, site.lat, site.tz); altitude > truthAltitude {
truthAltitude, truthJD = altitude, jd
}
}
truth := JD2DateByZone(truthJD-site.tz/24, location, false)
if got.Day() != local.Day() || got.Month() != local.Month() {
t.Fatalf("%s: sun culmination = %s, want local day %s", site.name, got.Format("2006-01-02 15:04"), local.Format("2006-01-02"))
}
if deviation := math.Abs(got.Sub(truth).Minutes()); deviation > 1.5 {
t.Fatalf("%s: sun culmination deviates %.2f min from the daily altitude maximum", site.name, deviation)
}
}
}
func TestMoonCulminationAnchorKeepsLocalDay(t *testing.T) {
timestamp := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
for _, site := range culminationAnchorSites {
local, midnightJD := culminationAnchorLocalMidnight(site, timestamp)
location := local.Location()
got := JD2DateByZone(MoonCulminationTime(midnightJD, site.lon, site.lat, site.tz), location, true)
truthJD, truthAltitude := 0.0, -999.0
for minute := 0; minute <= 24*60; minute++ {
jd := midnightJD + float64(minute)/1440.0
if altitude := HMoonHeight(jd, site.lon, site.lat, site.tz); altitude > truthAltitude {
truthAltitude, truthJD = altitude, jd
}
}
truth := JD2DateByZone(truthJD, location, true)
if got.Day() != local.Day() || got.Month() != local.Month() {
t.Fatalf("%s: moon culmination = %s, want local day %s", site.name, got.Format("2006-01-02 15:04"), local.Format("2006-01-02"))
}
if deviation := math.Abs(got.Sub(truth).Minutes()); deviation > 6.0 {
t.Fatalf("%s: moon culmination deviates %.2f min from the daily altitude maximum", site.name, deviation)
}
}
}
func TestSunCulminationAnchorDiffersFromMidnightAnchor(t *testing.T) {
site := culminationAnchorSites[0]
_, midnightJD := culminationAnchorLocalMidnight(site, time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
// 不补半天时 floor 落在相邻的 UT 日:这正是调用方必须补偿 +0.5 的原因。
uncompensated := CulminationTime(midnightJD, site.lon, site.tz) - site.tz/24
compensated := CulminationTime(midnightJD+0.5, site.lon, site.tz) - site.tz/24
if math.Abs(uncompensated-compensated) < 0.4 {
t.Fatalf("expected the uncompensated anchor to land on an adjacent day: %.6f vs %.6f", uncompensated, compensated)
}
}
+186
View File
@@ -0,0 +1,186 @@
package basic
import "math"
// DeltaTModel 标识一个可选的已发布 ΔT 模型 / a selectable published ΔT model.
type DeltaTModel string
const (
// DeltaTModelDefault 内置默认模型:Stephenson–Morrison–Hohenkerk 2016 + Morrison 2021 的样条与积分 lod。
// DeltaTModelDefault is the built-in model: the SMH2016 + Morrison 2021 spline and integrated lod.
DeltaTModelDefault DeltaTModel = ""
// DeltaTModelSMH2016 显式选择内置的 SMH2016 + Morrison 2021 模型。
// DeltaTModelSMH2016 selects the built-in SMH2016 + Morrison 2021 model explicitly.
DeltaTModelSMH2016 DeltaTModel = "smh2016"
// DeltaTModelMS2004 Morrison & Stephenson 2004 的长期抛物线 ΔT = −20 + 32u²(u = (y−1820)/100),单位秒。
// DeltaTModelMS2004 is the Morrison & Stephenson 2004 long-term parabola in seconds.
DeltaTModelMS2004 DeltaTModel = "ms2004"
// DeltaTModelEspenakMeeus2006 Espenak & Meeus 2006 的分段多项式,基于 M&S2004,未加配对修正 c。
// DeltaTModelEspenakMeeus2006 is the Espenak & Meeus 2006 piecewise polynomial without the pairing term c.
DeltaTModelEspenakMeeus2006 DeltaTModel = "espenak_meeus_2006"
// DeltaTModelNASACanon2006 是 Espenak–Meeus 再加配对修正 c = −0.000012932(y−1955)²(y < 1955),
// 即 NASA 五千年目录实际印刷的 ΔT 口径,配对的是 ELP2000/82 的 −25.858″/cy²。
// DeltaTModelNASACanon2006 adds the pairing term c for y < 1955, the convention printed by NASA's canon.
DeltaTModelNASACanon2006 DeltaTModel = "nasa_canon_2006"
// DeltaTModelManual 表示当前生效的是 SetDeltaTFn 注入的任意函数,不是命名模型。
// DeltaTModelManual reports that an arbitrary function injected with SetDeltaTFn is active.
DeltaTModelManual DeltaTModel = "manual"
)
// deltaTModelFunction 把命名模型包成 ΔT 钩子的签名;isJulianDay 为假时入参是十进制年。
func deltaTModelFunction(model DeltaTModel, keepObserved bool) func(float64, bool) float64 {
return func(date float64, isJulianDay bool) float64 {
if math.IsNaN(date) || math.IsInf(date, 0) {
return math.NaN()
}
if isJulianDay {
return DeltaTModelSeconds(model, date, keepObserved)
}
if keepObserved {
year := math.Floor(date)
start := JDCalc(int(year), 1, 1)
end := JDCalc(int(year)+1, 1, 1)
jd := start + (date-year)*(end-start)
if seconds, ok := deltaTMonthlyAt(jd); ok {
return seconds
}
}
return deltaTModelSecondsAtYear(model, date)
}
}
// DeltaTModelSeconds 按命名模型求 ΔT(秒),入参为 UT 儒略日。
//
// keepObserved 为真时,落在逐月实测表覆盖段内的时刻一律返回实测值,模型只接管表外;
// 这是"实测优先"的推荐用法,也是内置默认模型的结构。为假时全程使用模型,仅用于口径对照。
// DeltaTModelSeconds evaluates a named model. With keepObserved the monthly observed table wins
// wherever it covers the instant and the model only takes over outside it.
func DeltaTModelSeconds(model DeltaTModel, jd float64, keepObserved bool) float64 {
if math.IsNaN(jd) || math.IsInf(jd, 0) {
return math.NaN()
}
switch model {
case DeltaTModelDefault, DeltaTModelSMH2016:
if keepObserved {
// 与内置默认逐位一致(含实测段与样条表尾的常值锚定)。
return deltaTModelSecondsAtUT(jd)
}
return deltaTSplineAtJDE(jd)
}
if keepObserved {
if seconds, ok := deltaTMonthlyAt(jd); ok {
return seconds
}
}
return deltaTModelSecondsAtYear(model, deltaTYearAtJDE(jd))
}
// SetDeltaTModel 安装命名模型;keepObserved 为真时保留实测表覆盖段(推荐),未知模型返回 false 且不改动现状。
// SetDeltaTModel installs a named model, keeping the observed span when keepObserved is true;
// an unknown model returns false and leaves the current model untouched.
func SetDeltaTModel(model DeltaTModel, keepObserved bool) bool {
if !knownDeltaTModel(model) {
return false
}
if model == DeltaTModelDefault || model == DeltaTModelSMH2016 {
if keepObserved {
// 内置模型本身就是"实测优先",直接用它可以避免表尾锚定出现差异。
installDeltaTFn(DefaultDeltaTv2, model, true)
return true
}
installDeltaTFn(deltaTModelFunction(model, false), model, false)
return true
}
installDeltaTFn(deltaTModelFunction(model, keepObserved), model, keepObserved)
return true
}
// GetDeltaTModel 返回当前生效的模型与是否保留实测段;SetDeltaTFn 注入的任意函数报 DeltaTModelManual。
// GetDeltaTModel returns the active model and whether the observed span is kept; an arbitrary
// function injected with SetDeltaTFn reports DeltaTModelManual.
func GetDeltaTModel() (DeltaTModel, bool) {
deltaTFnMu.RLock()
defer deltaTFnMu.RUnlock()
return activeDeltaTModel, activeDeltaTKeepObserved
}
func knownDeltaTModel(model DeltaTModel) bool {
switch model {
case DeltaTModelDefault, DeltaTModelSMH2016, DeltaTModelMS2004,
DeltaTModelEspenakMeeus2006, DeltaTModelNASACanon2006:
return true
}
return false
}
// deltaTModelSecondsAtYear 按十进制年求模型值;实测段是否介入由调用方决定。
func deltaTModelSecondsAtYear(model DeltaTModel, year float64) float64 {
switch model {
case DeltaTModelMS2004:
u := (year - 1820) / 100
return -20 + 32*u*u
case DeltaTModelEspenakMeeus2006:
return espenakMeeus2006Seconds(year)
case DeltaTModelNASACanon2006:
value := espenakMeeus2006Seconds(year)
if year < 1955 {
// 配对修正:把 −26.0″/cy² 口径的 ΔT 换到 −25.858″/cy²(1955 起原子时使其与月球历表无关)。
d := year - 1955
value += -0.000012932 * d * d
}
return value
}
return DeltaTSplineY(year)
}
// espenakMeeus2006Seconds 是 Espenak & Meeus 2006 对 −1999..+3000 的分段多项式(秒),
// 自变量 y = year + (month−0.5)/12;分段边界处两段取值一致(−500 处由 17203.7 强制连续)。
func espenakMeeus2006Seconds(y float64) float64 {
switch {
case y < -500:
u := (y - 1820) / 100
return -20 + 32*u*u
case y < 500:
u := y / 100
return 10583.6 + u*(-1014.41+u*(33.78311+u*(-5.952053+u*(-0.1798452+u*(0.022174192+u*0.0090316521)))))
case y < 1600:
u := (y - 1000) / 100
return 1574.2 + u*(-556.01+u*(71.23472+u*(0.319781+u*(-0.8503463+u*(-0.005050998+u*0.0083572073)))))
case y < 1700:
t := y - 1600
return 120 - 0.9808*t - 0.01532*t*t + t*t*t/7129
case y < 1800:
t := y - 1700
return 8.83 + t*(0.1603+t*(-0.0059285+t*(0.00013336-t/1174000)))
case y < 1860:
t := y - 1800
return 13.72 + t*(-0.332447+t*(0.0068612+t*(0.0041116+t*(-0.00037436+
t*(0.0000121272+t*(-0.0000001699+t*0.000000000875))))))
case y < 1900:
t := y - 1860
return 7.62 + t*(0.5737+t*(-0.251754+t*(0.01680668+t*(-0.0004473624+t/233174))))
case y < 1920:
t := y - 1900
return -2.79 + t*(1.494119+t*(-0.0598939+t*(0.0061966-t*0.000197)))
case y < 1941:
t := y - 1920
return 21.20 + t*(0.84493+t*(-0.076100+t*0.0020936))
case y < 1961:
t := y - 1950
return 29.07 + t*(0.407-t/233+t*t/2547)
case y < 1986:
t := y - 1975
return 45.45 + t*(1.067-t/260-t*t/718)
case y < 2005:
t := y - 2000
return 63.86 + t*(0.3345+t*(-0.060374+t*(0.0017275+t*(0.000651814+t*0.00002373599))))
case y < 2050:
t := y - 2000
return 62.92 + t*(0.32217+t*0.005589)
case y < 2150:
u := (y - 1820) / 100
return -20 + 32*u*u - 0.5628*(2150-y)
}
u := (y - 1820) / 100
return -20 + 32*u*u
}
+94
View File
@@ -0,0 +1,94 @@
package basic
import (
"math"
"testing"
)
// ΔT 命名模型的契约:已发表锚点、分段连续性、实测段优先的混合语义与模型标记。
func TestDeltaTModelPublishedAnchors(t *testing.T) {
resetTimeScaleState(t)
check := func(name string, year, want, tolerance float64) {
t.Helper()
if got := DeltaT(year, false); math.Abs(got-want) > tolerance {
t.Fatalf("%s ΔT(%.5f)=%.6f, want %.6f±%g", name, year, got, want, tolerance)
}
}
if !SetDeltaTModel(DeltaTModelEspenakMeeus2006, false) {
t.Fatal("Espenak–Meeus 模型应可安装")
}
check("E-M", 2017.64, 70.34, 0.01)
check("E-M", 2024.27, 74.03, 0.01)
check("E-M", 2100, 202.7, 0.05)
if !SetDeltaTModel(DeltaTModelMS2004, false) {
t.Fatal("M&S2004 模型应可安装")
}
check("M&S2004", -500, 17203.7, 0.05)
check("M&S2004", 2100, 230.9, 0.05)
if !SetDeltaTModel(DeltaTModelEspenakMeeus2006, false) {
t.Fatal("Espenak–Meeus 模型应可安装")
}
// 分段边界:原表各段是独立拟合的,边界处本身留有台阶(实测最大 ≤0.3 s,1600 处约 0.25 s),
// 这里只守住这个量级,用来抓 Horner 展开之类的结构性错误。
for _, knot := range []float64{-500, 500, 1600, 1700, 1800, 1860, 1900, 1920, 1941, 1961, 1986, 2005, 2050, 2150} {
before, after := DeltaT(knot-1e-6, false), DeltaT(knot+1e-6, false)
if diff := math.Abs(after - before); diff > 0.3 {
t.Errorf("分段边界 %.0f 台阶过大:%.6f vs %.6f(差 %g)", knot, before, after, diff)
}
}
}
func TestDeltaTModelKeepsObservedSpan(t *testing.T) {
resetTimeScaleState(t)
jd := JDCalc(2026, 3, 1)
observed := DeltaT(jd, true)
if !SetDeltaTModel(DeltaTModelEspenakMeeus2006, true) {
t.Fatal("Espenak–Meeus 模型应可安装")
}
if got := DeltaT(jd, true); math.Abs(got-observed) > 1e-12 {
t.Fatalf("实测段应优先:got %.9f, want %.9f", got, observed)
}
pure := DeltaTModelSeconds(DeltaTModelEspenakMeeus2006, jd, false)
if math.Abs(pure-observed) < 5 {
t.Fatalf("纯模型值 %.3f 应与实测 %.3f 明显不同(否则测不到混合语义)", pure, observed)
}
if model, keep := GetDeltaTModel(); model != DeltaTModelEspenakMeeus2006 || !keep {
t.Fatalf("模型标记 = (%q, %v)", model, keep)
}
future := JDCalc(2100, 1, 1)
if got, want := DeltaT(future, true), DeltaTModelSeconds(DeltaTModelEspenakMeeus2006, future, false); math.Abs(got-want) > 1e-9 {
t.Fatalf("表外应走模型:got %.9f, want %.9f", got, want)
}
// SMH2016 混合档必须与内置默认逐位一致。
if !SetDeltaTModel(DeltaTModelSMH2016, true) {
t.Fatal("SMH2016 模型应可安装")
}
if got := DeltaT(jd, true); got != observed {
t.Fatalf("SMH2016 混合档 %.9f 与内置默认 %.9f 不一致", got, observed)
}
if got := DeltaT(future, true); got != DefaultDeltaTv2(future, true) {
t.Fatalf("SMH2016 混合档表外 %.9f 与内置默认 %.9f 不一致", got, DefaultDeltaTv2(future, true))
}
// 手工注入的任意函数标记为 manual;恢复 nil 回到内置默认。
SetDeltaTFn(func(float64, bool) float64 { return 0 })
if model, keep := GetDeltaTModel(); model != DeltaTModelManual || keep {
t.Fatalf("手工注入标记 = (%q, %v)", model, keep)
}
SetDeltaTFn(nil)
if model, keep := GetDeltaTModel(); model != DeltaTModelDefault || !keep {
t.Fatalf("恢复默认标记 = (%q, %v)", model, keep)
}
if SetDeltaTModel(DeltaTModel("nope"), true) {
t.Fatal("未知模型应被拒绝")
}
if model, _ := GetDeltaTModel(); model != DeltaTModelDefault {
t.Fatalf("被拒绝后模型不应改动,got %q", model)
}
}
+107
View File
@@ -0,0 +1,107 @@
package basic
import (
"math"
"testing"
)
func TestDefaultDeltaTFractionalYear(t *testing.T) {
for _, year := range []float64{-700.5, -0.5, 0, 1000.5, 1582.75, 1800.5, 2000.5, 2026.5} {
whole := math.Floor(year)
start := JDCalc(int(whole), 1, 1)
end := JDCalc(int(whole)+1, 1, 1)
want := DefaultDeltaTv2(start+(year-whole)*(end-start), true)
got := DefaultDeltaTv2(year, false)
if math.IsNaN(got) || math.Abs(got-want) > 1e-10 {
t.Errorf("year=%v DeltaT=%v, want %v", year, got, want)
}
}
}
func TestDefaultDeltaTInvalidInput(t *testing.T) {
for _, value := range []float64{math.NaN(), math.Inf(1), math.Inf(-1)} {
for _, isJD := range []bool{false, true} {
if got := DefaultDeltaTv2(value, isJD); !math.IsNaN(got) {
t.Errorf("DeltaT(%v, %v)=%v, want NaN", value, isJD, got)
}
}
}
}
// 闰秒只改 TT−UTC,不改 TT−UT1:ΔT 在闰秒两侧连续,TT−UTC 才跳 1 秒。
func TestDeltaTStaysContinuousAcrossLeapSecond(t *testing.T) {
boundary := 2457754.5
before := DeltaTv2(boundary - 1e-6)
after := DeltaTv2(boundary + 1e-6)
if math.Abs(after-before) > 1e-3 {
t.Fatalf("ΔT should stay continuous at a leap second: %v → %v", before, after)
}
if step := TTMinusUTCSeconds(boundary+1e-6) - TTMinusUTCSeconds(boundary-1e-6); math.Abs(step-1) > 1e-9 {
t.Fatalf("TT−UTC should step by one second: %v", step)
}
if got := DeltaTSplineY(math.NaN()); !math.IsNaN(got) {
t.Fatalf("DeltaTSplineY(NaN)=%v, want NaN", got)
}
}
func TestUTC2TTRoundTrip(t *testing.T) {
for _, year := range []int{-2000, -720, 0, 26, 1426, 2025, 2027, 3627, 4026, 5000} {
utc := JDCalc(year, 9, 20.123456)
tt := UTC2TT(utc)
if got := TT2UTC(tt); math.Abs(got-utc) > math.Nextafter(utc, math.Inf(1))-utc {
t.Errorf("year=%d UTC round trip differs by %.9f seconds", year, (got-utc)*86400)
}
if got := UTC2TT(TT2UTC(tt)); got != tt {
t.Errorf("year=%d TT round trip differs by %.9f seconds", year, (got-tt)*86400)
}
}
for _, seconds := range []float64{-70, -1, -0.1, 0, 0.1, 1, 70} {
utc := 2457754.5 + seconds/86400
if got := TT2UTC(UTC2TT(utc)); got != utc {
t.Errorf("leap second offset=%g round trip differs by %.9f seconds", seconds, (got-utc)*86400)
}
}
}
func TestUTC2TTRoundTripUnderCustomDeltaT(t *testing.T) {
original := GetDeltaTFn()
t.Cleanup(func() { SetDeltaTFn(original) })
for _, slope := range []float64{0, 0.01} {
SetDeltaTFn(func(jd float64, isJD bool) float64 {
if !isJD {
t.Fatal("conversion must request Delta T at a Julian day")
}
return 10000 + slope*(jd-2451545)
})
utc := 3000000.123456
if got := TT2UTC(UTC2TT(utc)); got != utc {
t.Errorf("custom slope=%g round trip differs by %.9f seconds", slope, (got-utc)*86400)
}
}
}
// DeltaTSecondsAt:有限值(含 0)是显式覆盖,NaN/±Inf 才回退进程级模型;模型按 UT 求值。
func TestDeltaTSecondsAtOverrideContract(t *testing.T) {
jd := 2460310.5
if got := DeltaTSecondsAt(jd, 0); got != 0 {
t.Fatalf("explicit zero override = %v, want 0", got)
}
if got := DeltaTSecondsAt(jd, 12.5); got != 12.5 {
t.Fatalf("explicit override = %v, want 12.5", got)
}
model := DeltaTSecondsAt(jd, math.NaN())
if !finite(model) || model <= 0 {
t.Fatalf("model fallback = %v, want a positive finite value", model)
}
if got := DeltaTSecondsAt(jd, math.Inf(1)); got != model {
t.Fatalf("+Inf override = %v, want the model %v", got, model)
}
if got := deltaTModelSecondsAtTT(jd); got != model {
t.Fatalf("deltaTModelSecondsAtTT = %v, want %v", got, model)
}
// 模型按 UT 键控:与"把 TT 直接当 UT"的朴素求值有微小差别。
naive := DeltaT(jd, true)
if diff := math.Abs(naive - model); diff > 1e-3 {
t.Fatalf("UT-keyed model differs from naive TT evaluation by %v s", diff)
}
}
+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(jde float64) float64 {
return SunSemidiameterN(jde, -1)
}
// SunSemidiameterN 太阳视半径(截断版),单位角秒 / truncated apparent solar semidiameter in arcseconds.
func SunSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(sunEquatorialRadiusKM, EarthAwayN(jde, n))
}
// SunDiameter 太阳视直径,单位角秒 / apparent solar diameter in arcseconds.
func SunDiameter(jde float64) float64 {
return SunDiameterN(jde, -1)
}
// SunDiameterN 太阳视直径(截断版),单位角秒 / truncated apparent solar diameter in arcseconds.
func SunDiameterN(jde float64, n int) float64 {
return 2 * SunSemidiameterN(jde, n)
}
// MoonSemidiameter 月亮视半径,单位角秒 / apparent lunar semidiameter in arcseconds.
func MoonSemidiameter(jde float64) float64 {
return MoonSemidiameterN(jde, -1)
}
// MoonSemidiameterN 月亮视半径(截断版),单位角秒 / truncated apparent lunar semidiameter in arcseconds.
func MoonSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterArcsec(moonEquatorialRadiusKM, HMoonAwayN(jde, n))
}
// MoonDiameter 月亮视直径,单位角秒 / apparent lunar diameter in arcseconds.
func MoonDiameter(jde float64) float64 {
return MoonDiameterN(jde, -1)
}
// MoonDiameterN 月亮视直径(截断版),单位角秒 / truncated apparent lunar diameter in arcseconds.
func MoonDiameterN(jde float64, n int) float64 {
return 2 * MoonSemidiameterN(jde, n)
}
// MercurySemidiameter 水星视半径,单位角秒 / apparent Mercury semidiameter in arcseconds.
func MercurySemidiameter(jde float64) float64 {
return MercurySemidiameterN(jde, -1)
}
// MercurySemidiameterN 水星视半径(截断版),单位角秒 / truncated apparent Mercury semidiameter in arcseconds.
func MercurySemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(mercuryEquatorialRadiusKM, EarthMercuryAwayN(jde, n))
}
// MercuryDiameter 水星视直径,单位角秒 / apparent Mercury diameter in arcseconds.
func MercuryDiameter(jde float64) float64 {
return MercuryDiameterN(jde, -1)
}
// MercuryDiameterN 水星视直径(截断版),单位角秒 / truncated apparent Mercury diameter in arcseconds.
func MercuryDiameterN(jde float64, n int) float64 {
return 2 * MercurySemidiameterN(jde, n)
}
// VenusSemidiameter 金星视半径,单位角秒 / apparent Venus semidiameter in arcseconds.
func VenusSemidiameter(jde float64) float64 {
return VenusSemidiameterN(jde, -1)
}
// VenusSemidiameterN 金星视半径(截断版),单位角秒 / truncated apparent Venus semidiameter in arcseconds.
func VenusSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(venusEquatorialRadiusKM, EarthVenusAwayN(jde, n))
}
// VenusDiameter 金星视直径,单位角秒 / apparent Venus diameter in arcseconds.
func VenusDiameter(jde float64) float64 {
return VenusDiameterN(jde, -1)
}
// VenusDiameterN 金星视直径(截断版),单位角秒 / truncated apparent Venus diameter in arcseconds.
func VenusDiameterN(jde float64, n int) float64 {
return 2 * VenusSemidiameterN(jde, n)
}
// MarsSemidiameter 火星视半径,单位角秒 / apparent Mars semidiameter in arcseconds.
func MarsSemidiameter(jde float64) float64 {
return MarsSemidiameterN(jde, -1)
}
// MarsSemidiameterN 火星视半径(截断版),单位角秒 / truncated apparent Mars semidiameter in arcseconds.
func MarsSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(marsEquatorialRadiusKM, EarthMarsAwayN(jde, n))
}
// MarsDiameter 火星视直径,单位角秒 / apparent Mars diameter in arcseconds.
func MarsDiameter(jde float64) float64 {
return MarsDiameterN(jde, -1)
}
// MarsDiameterN 火星视直径(截断版),单位角秒 / truncated apparent Mars diameter in arcseconds.
func MarsDiameterN(jde float64, n int) float64 {
return 2 * MarsSemidiameterN(jde, n)
}
// JupiterSemidiameter 木星视半径,单位角秒 / apparent Jupiter semidiameter in arcseconds.
func JupiterSemidiameter(jde float64) float64 {
return JupiterSemidiameterN(jde, -1)
}
// JupiterSemidiameterN 木星视半径(截断版),单位角秒 / truncated apparent Jupiter semidiameter in arcseconds.
func JupiterSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(jupiterEquatorialRadiusKM, EarthJupiterAwayN(jde, n))
}
// JupiterDiameter 木星视直径,单位角秒 / apparent Jupiter diameter in arcseconds.
func JupiterDiameter(jde float64) float64 {
return JupiterDiameterN(jde, -1)
}
// JupiterDiameterN 木星视直径(截断版),单位角秒 / truncated apparent Jupiter diameter in arcseconds.
func JupiterDiameterN(jde float64, n int) float64 {
return 2 * JupiterSemidiameterN(jde, n)
}
// SaturnSemidiameter 土星视半径,单位角秒 / apparent Saturn semidiameter in arcseconds.
func SaturnSemidiameter(jde float64) float64 {
return SaturnSemidiameterN(jde, -1)
}
// SaturnSemidiameterN 土星视半径(截断版),单位角秒 / truncated apparent Saturn semidiameter in arcseconds.
func SaturnSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(saturnEquatorialRadiusKM, EarthSaturnAwayN(jde, n))
}
// SaturnDiameter 土星视直径,单位角秒 / apparent Saturn diameter in arcseconds.
func SaturnDiameter(jde float64) float64 {
return SaturnDiameterN(jde, -1)
}
// SaturnDiameterN 土星视直径(截断版),单位角秒 / truncated apparent Saturn diameter in arcseconds.
func SaturnDiameterN(jde float64, n int) float64 {
return 2 * SaturnSemidiameterN(jde, n)
}
// UranusSemidiameter 天王星视半径,单位角秒 / apparent Uranus semidiameter in arcseconds.
func UranusSemidiameter(jde float64) float64 {
return UranusSemidiameterN(jde, -1)
}
// UranusSemidiameterN 天王星视半径(截断版),单位角秒 / truncated apparent Uranus semidiameter in arcseconds.
func UranusSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(uranusEquatorialRadiusKM, EarthUranusAwayN(jde, n))
}
// UranusDiameter 天王星视直径,单位角秒 / apparent Uranus diameter in arcseconds.
func UranusDiameter(jde float64) float64 {
return UranusDiameterN(jde, -1)
}
// UranusDiameterN 天王星视直径(截断版),单位角秒 / truncated apparent Uranus diameter in arcseconds.
func UranusDiameterN(jde float64, n int) float64 {
return 2 * UranusSemidiameterN(jde, n)
}
// NeptuneSemidiameter 海王星视半径,单位角秒 / apparent Neptune semidiameter in arcseconds.
func NeptuneSemidiameter(jde float64) float64 {
return NeptuneSemidiameterN(jde, -1)
}
// NeptuneSemidiameterN 海王星视半径(截断版),单位角秒 / truncated apparent Neptune semidiameter in arcseconds.
func NeptuneSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(neptuneEquatorialRadiusKM, EarthNeptuneAwayN(jde, n))
}
// NeptuneDiameter 海王星视直径,单位角秒 / apparent Neptune diameter in arcseconds.
func NeptuneDiameter(jde float64) float64 {
return NeptuneDiameterN(jde, -1)
}
// NeptuneDiameterN 海王星视直径(截断版),单位角秒 / truncated apparent Neptune diameter in arcseconds.
func NeptuneDiameterN(jde float64, n int) float64 {
return 2 * NeptuneSemidiameterN(jde, 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 := UTC2TT(Date2JD(date.UTC()))
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 := UTC2TT(Date2JD(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)))
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))
}
}
+8 -7
View File
@@ -1,8 +1,9 @@
package basic
import (
. "b612.me/astro/tools"
"math"
. "b612.me/astro/tools"
)
//地球常数
@@ -20,22 +21,22 @@ 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 地心直径与纬度的关系
// GeocentricRadius 给定纬度处的地心半径,单位米 / geocentric radius at the given latitude, in metres.
func GeocentricRadius(lat float64) float64 {
a := (EARTH_EQUATORIAL_RADIUS * EARTH_EQUATORIAL_RADIUS * Cos(lat))
a *= a
+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(JDCalc(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(
UTC2TT(Date2JD(time.Date(2024, 4, 8, 12, 0, 0, 0, time.UTC))),
-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")
}
}
+77
View File
@@ -0,0 +1,77 @@
package basic
import "math"
const (
exactEventTolerance = 2.0 / 86400.0
exactQueryTTToleranceUT = 0.1 / 86400.0
// stationQueryToleranceUT 站(留)事件的「同刻」容差 / same-instant tolerance for station events.
//
// 站的求解本身有数值不确定度(火星 ~0.2 s、金星 ~2 s、外行星改精修步长后 <0.05 s)。
// 若与普通事件一样取 0.1 s,查询落在站前几十毫秒时会被判成「站仍在未来」,
// 于是跳到上一个会合周期。取 0.5 s 可覆盖除金星外的全部不确定度。
// 上限约束:必须明显小于 1 s,否则会破坏
// TestInnerPlanetNextEventAdvancesPastReturnedEvent 的「查询=事件+1 秒必须前进」语义。
stationQueryToleranceUT = 0.5 / 86400.0
)
func sameEventJD(a, b float64) bool {
return math.Abs(a-b) <= exactEventTolerance
}
// stationUTQueryBeforeOrEqual / stationUTQueryAfterOrEqual 站事件专用侧向判定,
// 容差比普通事件宽(见 stationQueryToleranceUT)。
func stationUTQueryBeforeOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) <= stationQueryToleranceUT
}
func stationUTQueryAfterOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) >= -stationQueryToleranceUT
}
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
// inclusive* 的唯一作用是把「查询几乎正好落在事件上」判成包含:
// 反向搜索给出的邻接事件落在同刻容差内时优先返回它,否则返回本方向的事件(可能是 NaN)。
func inclusiveLastPhaseEvent(jde, degree float64, fn phaseEventSearchFunc) float64 {
last := fn(jde, degree, 0)
if next := fn(jde, degree, 1); eventUTQueryBeforeOrEqual(next, jde) && eventUTQueryAfterOrEqual(next, jde) {
return next
}
return last
}
func inclusiveNextPhaseEvent(jde, degree float64, fn phaseEventSearchFunc) float64 {
if last := fn(jde, degree, 0); eventUTQueryBeforeOrEqual(last, jde) && eventUTQueryAfterOrEqual(last, jde) {
return last
}
return fn(jde, degree, 1)
}
func inclusiveLastSimpleEvent(jde float64, lastFn, nextFn simpleEventSearchFunc) float64 {
last := lastFn(jde)
if next := nextFn(jde); eventUTQueryBeforeOrEqual(next, jde) && eventUTQueryAfterOrEqual(next, jde) {
return next
}
return last
}
func inclusiveNextSimpleEvent(jde float64, lastFn, nextFn simpleEventSearchFunc) float64 {
if last := lastFn(jde); eventUTQueryBeforeOrEqual(last, jde) && eventUTQueryAfterOrEqual(last, jde) {
return last
}
return nextFn(jde)
}
+270
View File
@@ -0,0 +1,270 @@
package basic
import "math"
const (
eventNewtonMaxIterations = 24
eventDirectionalSearchIterations = 128
eventRiseSetScanStep = 1.0 / 1440
// stationDerivativeStepDay 「留」精修用的中心差分步长(天)。
// 0.5 秒级的步长会被浮点相消噪声支配(实测外行星留误差可达 42 s);
// 0.01 天(14.4 分钟)实测误差 <0.05 s,且求值次数不变。
stationDerivativeStepDay = 0.01
)
func isFiniteFloat(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
// eventNewtonRefine 执行有界牛顿迭代;修正函数返回 f(x)/f'(x),调用者保留现有导数计算 / eventNewtonRefine performs a bounded Newton iteration. The correction
// 对格式错误输入和不收敛迭代快速失败 / function returns f(x)/f'(x), so callers retain their existing derivative
// 计算 / calculation while malformed input and non-convergent iterations fail fast.
func eventNewtonRefine(seed, tolerance float64, correction func(float64) float64) (float64, bool) {
if !isFiniteFloat(seed) || !isFiniteFloat(tolerance) || tolerance <= 0 {
return math.NaN(), false
}
current := seed
for i := 0; i < eventNewtonMaxIterations; i++ {
step := correction(current)
if !isFiniteFloat(step) {
return math.NaN(), false
}
next := current - step
if !isFiniteFloat(next) {
return math.NaN(), false
}
if math.Abs(next-current) <= tolerance {
return next, true
}
current = next
}
return math.NaN(), false
}
func eventRiseSetCandidateValid(candidate, civilDayStart, slope float64, isRise bool) bool {
if !isFiniteFloat(candidate) || !isFiniteFloat(civilDayStart) || !isFiniteFloat(slope) ||
candidate < civilDayStart || candidate >= civilDayStart+1 {
return false
}
if isRise {
return slope > 0
}
return slope < 0
}
func eventDirectionalRiseSetSearch(civilDayStart float64, isRise bool, fallbackErr error,
residual func(float64) float64) (float64, error) {
if !isFiniteFloat(civilDayStart) {
return 0, ErrInvalidObservationInput
}
previousJD := civilDayStart
previousValue := residual(previousJD)
if !isFiniteFloat(previousValue) {
return 0, ErrInvalidObservationInput
}
minimum, maximum := previousValue, previousValue
steps := int(math.Round(1 / eventRiseSetScanStep))
for i := 1; i <= steps; i++ {
currentJD := civilDayStart + float64(i)*eventRiseSetScanStep
currentValue := residual(currentJD)
if !isFiniteFloat(currentValue) {
return 0, ErrInvalidObservationInput
}
minimum = math.Min(minimum, currentValue)
maximum = math.Max(maximum, currentValue)
if eventCrossesDirection(previousValue, currentValue, isRise) {
eventJD := eventDirectionalBracketRefine(previousJD, currentJD, previousValue, currentValue, residual)
if eventJD < civilDayStart+1 {
return eventJD, nil
}
}
previousJD = currentJD
previousValue = currentValue
}
switch {
case fallbackErr != nil:
return 0, fallbackErr
case maximum < 0:
return 0, ErrNeverRise
case minimum > 0:
return 0, ErrNeverSet
default:
return 0, ErrNotOnThisDate
}
}
func eventCrossesDirection(leftValue, rightValue float64, isRise bool) bool {
if isRise {
return leftValue <= 0 && rightValue >= 0 && leftValue != rightValue
}
return leftValue >= 0 && rightValue <= 0 && leftValue != rightValue
}
func eventDirectionalBracketRefine(leftJD, rightJD, leftValue, rightValue float64, residual func(float64) float64) float64 {
if leftValue == 0 {
return leftJD
}
if rightValue == 0 {
return rightJD
}
for i := 0; i < 48; i++ {
middleJD := (leftJD + rightJD) / 2
middleValue := residual(middleJD)
if middleValue == 0 {
return middleJD
}
if (leftValue < 0) == (middleValue < 0) {
leftJD = middleJD
leftValue = middleValue
} else {
rightJD = middleJD
}
}
return (leftJD + rightJD) / 2
}
// eventBracketSecantRoot 在已知异号的括号内用割线法(带中点兜底)求根。
//
// 与 eventDirectionalBracketRefine(固定 48 次二分)相比,割线法通常 5~8 次求值即可达到
// 亚秒精度,适合「括号由廉价截断级数给出、抛光必须用全项级数」的两段式搜索。
// 括号每一步都收缩,因此不会跑到括号外;括号端点同号或出现非有限值时返回 false。
func eventBracketSecantRoot(leftJD, rightJD, leftValue, rightValue, tolerance float64,
fn func(float64) float64) (float64, bool) {
if leftValue == 0 {
return leftJD, true
}
if rightValue == 0 {
return rightJD, true
}
if !isFiniteFloat(leftValue) || !isFiniteFloat(rightValue) || leftValue*rightValue > 0 {
return math.NaN(), false
}
if !isFiniteFloat(tolerance) || tolerance <= 0 {
tolerance = 0.5 / 86400.0
}
bestJD := (leftJD + rightJD) / 2
for i := 0; i < 48; i++ {
candidateJD := (leftJD + rightJD) / 2
if rightValue != leftValue {
secantJD := rightJD - rightValue*(rightJD-leftJD)/(rightValue-leftValue)
if secantJD > leftJD && secantJD < rightJD {
candidateJD = secantJD
}
}
candidateValue := fn(candidateJD)
if !isFiniteFloat(candidateValue) {
return math.NaN(), false
}
bestJD = candidateJD
if candidateValue == 0 || math.Abs(rightJD-leftJD) <= tolerance {
return candidateJD, true
}
if (leftValue < 0) == (candidateValue < 0) {
leftJD, leftValue = candidateJD, candidateValue
continue
}
rightJD, rightValue = candidateJD, candidateValue
}
if math.Abs(rightJD-leftJD) <= tolerance {
return bestJD, true
}
// 48 次迭代后括号仍宽于容差(例如容差低于该儒略日的 ULP):调用方把 ok 当作
// “已抛光到容差”,这里必须报 false,不能返回一个精度未达标的时刻。
return bestJD, false
}
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
}
+41
View File
@@ -0,0 +1,41 @@
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)
}
}
// 48 次迭代用尽后括号仍宽于容差时不能谎报"已抛光":调用方把 ok 当作精度保证。
func TestEventBracketSecantRootRejectsUnpolishedRoot(t *testing.T) {
linear := func(x float64) float64 { return x - 0.5 }
if _, ok := eventBracketSecantRoot(0, 1, -0.5, 0.5, 1e-9, linear); !ok {
t.Fatalf("normal tolerance should return ok=true")
}
// 无理根(sqrt(2))永远取不到精确零,容差又低于该量级的 ULP:必须报 false。
irrational := func(x float64) float64 { return x*x - 2 }
if _, ok := eventBracketSecantRoot(0, 2, -2, 2, 1e-25, irrational); ok {
t.Fatalf("unreachable tolerance must not report a polished root")
}
if _, ok := eventBracketSecantRoot(0, 2, -2, 2, 1e-12, irrational); !ok {
t.Fatalf("reachable tolerance should still report ok=true")
}
}
+126
View File
@@ -0,0 +1,126 @@
package basic
import (
"math"
"time"
)
// 等时线(食甚/掩甚时刻等值线)的共享常量与工具。
// 两个等时线实现(日食、月掩)用同一套容差与时刻取值生成规则,避免口径漂移。
const (
// greatestTimeContourCoverToleranceKM 是"该点是否已被已绘等时线覆盖"的距离门限。
// 判据必须用点到折线的距离:同一条曲线被两个种子各画一次时该距离约为 0,而折线弦高在
// 1.5° 步长下最大几 km,所以 10 km 既不会漏判重复,也远小于不同支路的间距。
greatestTimeContourCoverToleranceKM = 10.0
// greatestTimeContourCorrectionBacktracking 是牛顿投影跳出定义域时按二分回退的次数。
greatestTimeContourCorrectionBacktracking = 6
// greatestTimeContourKMPerDegree 是地面大圆每度的近似长度,用于局部平面投影。
greatestTimeContourKMPerDegree = 111.195
// greatestTimeContourMaxLevels 是单个事件允许的等时线条数上限;超出部分按时间截断,
// 避免极长事件请求出上万条曲线。
greatestTimeContourMaxLevels = 64
// greatestTimeContourResidualTolerance 是残差零点判定的收敛容差。
greatestTimeContourResidualTolerance = 1e-9
// greatestTimeContourBisectionIterations 是二分求根的迭代上限。
greatestTimeContourBisectionIterations = 64
)
// greatestTimeContourDifference 优先用中心差分,一侧越界时退化为单侧差分。
func greatestTimeContourDifference(center, positive, negative, step float64, positiveOK, negativeOK bool) (float64, bool) {
switch {
case positiveOK && negativeOK:
return (positive - negative) / (2 * step), true
case positiveOK:
return (positive - center) / step, true
case negativeOK:
return (center - negative) / step, true
}
return 0, false
}
// greatestTimeContourBisect 在 [left,right] 上二分求残差零点;residual 在定义域外返回 NaN。
func greatestTimeContourBisect(
residual func(longitude, latitude float64) float64,
left, right, latitude, leftValue float64,
) (float64, bool) {
for iteration := 0; iteration < greatestTimeContourBisectionIterations; iteration++ {
middle := (left + right) / 2
middleValue := residual(middle, latitude)
if !finite(middleValue) {
return 0, false
}
if math.Abs(middleValue) <= greatestTimeContourResidualTolerance || right-left <= 1e-9 {
return middle, true
}
if leftValue*middleValue <= 0 {
right = middle
} else {
left, leftValue = middle, middleValue
}
}
return (left + right) / 2, true
}
// greatestTimeContourTickTolerance 是对齐网格在窗口边界上的容差,远小于任何合法步长。
const greatestTimeContourTickTolerance = time.Millisecond
// greatestTimeContourLevel 是一条等时线对应的时刻取值:TT 儒略日与它对应的时刻必须成对传递,
// 前者用于求根,后者用于标注(按步长生成时它是原始对齐时刻,避免 JDE 往返把整分截断成前一分钟)。
type greatestTimeContourLevel struct {
tt float64
at time.Time
}
// greatestTimeContourAlignedLevels 生成覆盖 [startTT,endTT] 且对齐到 step 整刻度的时刻取值,
// 最多 maxLevels 条;起点或终点恰好落在整刻度上时该刻度只生成一次。
func greatestTimeContourAlignedLevels(startTT, endTT float64, step time.Duration, maxLevels int) []greatestTimeContourLevel {
if step <= 0 || maxLevels <= 0 || endTT <= startTT {
return nil
}
start := greatestTimeContourTTToUTC(startTT)
first := start.Truncate(step)
// TT 儒略日往返有约 40 µs(1 ULP)误差,恰好落在整刻度上的窗口边界会算到刻度外一点点,容差按 1 ms 给。
if first.Add(greatestTimeContourTickTolerance).Before(start) {
first = first.Add(step)
}
end := greatestTimeContourTTToUTC(endTT)
levels := make([]greatestTimeContourLevel, 0, maxLevels)
for current := first; !current.Add(-greatestTimeContourTickTolerance).After(end) && len(levels) < maxLevels; current = current.Add(step) {
levels = append(levels, greatestTimeContourLevel{tt: greatestTimeContourUTCToTT(current), at: current})
}
return levels
}
// greatestTimeContourTTToUTC 把 TT 儒略日换成对应的 UTC 时刻。
func greatestTimeContourTTToUTC(tt float64) time.Time {
return JD2DateByZone(TT2UTC(tt), time.UTC, false)
}
// greatestTimeContourUTCToTT 把 UTC 时刻换成对应的 TT 儒略日。
func greatestTimeContourUTCToTT(value time.Time) float64 {
return UTC2TT(Date2JD(value.UTC()))
}
// greatestTimeContourPointSegmentKM 返回点到折线段的距离,单位 km。
// 段长只有 1–2 度,用局部等距圆柱近似即可,误差远小于覆盖容差。
func greatestTimeContourPointSegmentKM(longitude, latitude, startLongitude, startLatitude, endLongitude, endLatitude float64) float64 {
cosine := math.Cos(latitude * rad)
toX := func(value float64) float64 {
return normalizeLongitude(value-startLongitude) * rad * cosine * greatestTimeContourKMPerDegree
}
toY := func(value float64) float64 { return (value - startLatitude) * greatestTimeContourKMPerDegree }
pointX, pointY := toX(longitude), toY(latitude)
endX, endY := toX(endLongitude), toY(endLatitude)
lengthSquared := endX*endX + endY*endY
projection := 0.0
if lengthSquared > 0 {
projection = (pointX*endX + pointY*endY) / lengthSquared
if projection < 0 {
projection = 0
} else if projection > 1 {
projection = 1
}
}
return math.Hypot(pointX-projection*endX, pointY-projection*endY)
}
+66
View File
@@ -0,0 +1,66 @@
package basic
import (
"testing"
"time"
)
// 对齐网格是导出行为(等时线按整刻度取值)的契约,取值本身写成字面量以免与实际生成函数互相自证。
func TestGreatestTimeContourAlignedLevelsGridContracts(t *testing.T) {
tt := func(hour, minute int) float64 {
return UTC2TT(Date2JD(time.Date(2024, 1, 1, hour, minute, 0, 0, time.UTC)))
}
cases := []struct {
name string
start, end float64
step time.Duration
maxLevels int
want []string
}{
{
name: "起点在刻度之间时从下一个整刻度开始", start: tt(0, 7), end: tt(2, 7),
step: 30 * time.Minute, maxLevels: 64,
want: []string{"2024-01-01T00:30:00Z", "2024-01-01T01:00:00Z", "2024-01-01T01:30:00Z", "2024-01-01T02:00:00Z"},
},
{
name: "起点恰在刻度上时不重复生成该刻度", start: tt(0, 30), end: tt(2, 0),
step: 30 * time.Minute, maxLevels: 64,
want: []string{"2024-01-01T00:30:00Z", "2024-01-01T01:00:00Z", "2024-01-01T01:30:00Z", "2024-01-01T02:00:00Z"},
},
{
name: "终点恰在刻度上时包含终点", start: tt(0, 7), end: tt(1, 30),
step: 30 * time.Minute, maxLevels: 64,
want: []string{"2024-01-01T00:30:00Z", "2024-01-01T01:00:00Z", "2024-01-01T01:30:00Z"},
},
{
name: "上限截断", start: tt(0, 7), end: tt(5, 7),
step: 30 * time.Minute, maxLevels: 3,
want: []string{"2024-01-01T00:30:00Z", "2024-01-01T01:00:00Z", "2024-01-01T01:30:00Z"},
},
{
name: "窗口内没有整刻度时为空", start: tt(0, 1), end: tt(0, 20),
step: time.Hour, maxLevels: 64,
want: nil,
},
{
name: "非正步长或倒置窗口为空", start: tt(0, 7), end: tt(2, 7),
step: 0, maxLevels: 64, want: nil,
},
}
for _, testCase := range cases {
levels := greatestTimeContourAlignedLevels(testCase.start, testCase.end, testCase.step, testCase.maxLevels)
if len(levels) != len(testCase.want) {
t.Fatalf("%s: %d levels, want %d", testCase.name, len(levels), len(testCase.want))
}
for index, level := range levels {
got := level.at.UTC().Format(time.RFC3339)
if got != testCase.want[index] {
t.Fatalf("%s: level %d at %s, want %s", testCase.name, index, got, testCase.want[index])
}
// 标注时刻与求根时刻必须成对:TT 往返 1 ULP 内一致。
if roundTrip := greatestTimeContourUTCToTT(level.at); roundTrip != level.tt {
t.Fatalf("%s: level %d tt %.9f != %.9f", testCase.name, index, level.tt, roundTrip)
}
}
}
}
+189
View File
@@ -0,0 +1,189 @@
package basic
import "math"
const (
innerEventEpsilon = 0.1 / 86400.0
innerEventWindowPadding = 4.0 / 86400.0
innerEventMaximizeEpsilon = 4.0 / 86400.0
)
func eventQueryTTAsUT(queryTT float64) float64 {
return TT2UTC(queryTT)
}
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 UTC2TT(eventUT) + 1.0
}
func eventUTLastQueryTT(eventUT float64) float64 {
return UTC2TT(eventUT) - 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 <= innerEventMaximizeEpsilon {
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
}
+105
View File
@@ -0,0 +1,105 @@
package basic
import (
"testing"
"time"
)
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 := UTC2TT(tc.seed)
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 TestInnerPlanetNextEventAdvancesPastReturnedEvent(t *testing.T) {
cases := []struct {
name string
seed float64
next func(float64) float64
}{
{name: "MercuryConjunction", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryConjunction},
{name: "MercuryInferior", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryInferiorConjunction},
{name: "MercuryP2R", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryProgradeToRetrograde},
{name: "MercuryEastElongation", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryGreatestElongationEast},
{name: "VenusConjunction", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextVenusConjunction},
{name: "VenusWestElongation", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextVenusGreatestElongationWest},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
first := tc.next(tc.seed)
query := UTC2TT(Date2JD(JD2DateByZone(first, time.UTC, false).Add(time.Second)))
next := tc.next(query)
if !eventUTQueryAfterOrEqual(next, query) {
t.Fatalf("next should be after query: first=%.12f query=%.12f next=%.12f", first, query, next)
}
if sameEventJD(next, first) {
t.Fatalf("next should advance past first event: first=%.12f next=%.12f", first, next)
}
})
}
}
func TestInnerPlanetTypedConjunctionExactBoundaryIncludesCurrent(t *testing.T) {
cases := []struct {
name string
seed float64
next func(float64) float64
last func(float64) float64
}{
{name: "MercuryInferior", seed: NextMercuryInferiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextMercuryInferiorConjunction, last: LastMercuryInferiorConjunction},
{name: "MercurySuperior", seed: NextMercurySuperiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextMercurySuperiorConjunction, last: LastMercurySuperiorConjunction},
{name: "VenusInferior", seed: NextVenusInferiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextVenusInferiorConjunction, last: LastVenusInferiorConjunction},
{name: "VenusSuperior", seed: NextVenusSuperiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextVenusSuperiorConjunction, last: LastVenusSuperiorConjunction},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
queryTT := UTC2TT(tc.seed)
last := tc.last(queryTT)
next := tc.next(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)
}
})
}
}
+177
View File
@@ -0,0 +1,177 @@
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":
// 2197 年附近双方 ΔT 外推已差约 2 分钟(校正了 EarthPI 的 0.00046·T² 项之后,
// 三个 2197 样本从 −119.5 s 移到 −121…−126 s),因此与留点同样留 3 分钟余量;
// 真实的搜索错误是小时/天级,靠这个容差仍能抓住。
// Around 2197 the two ΔT extrapolations already differ by about two minutes (after
// correcting the EarthPI 0.00046*T^2 term, three 2197 samples moved from -119.5 s to
// -121..-126 s), so conjunctions get the same 3-minute margin as the stations.
// Real search errors are hours or days, so this still catches them.
return 3 * time.Minute
case "P2R", "R2P":
// 留是 RA 的极值点:星历模型中 ~1e-6 度/天的变化率差异就会被放大成分钟级时刻差,
// 且 JPL 给出的 UT 时刻还叠加了各自的 ΔT 外推(2197 年附近两者已差 ~2 分钟)。
// 这里留 3 分钟余量,仍远小于任何真实的搜索错误(小时/天级)。
return 3 * 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 := JD2DateByZone(nextFn(toUTJD(before)), when.Location(), false)
last := JD2DateByZone(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)
})
}
}
+185
View File
@@ -0,0 +1,185 @@
package basic
import (
"errors"
"math"
"time"
)
var ErrInvalidCivilDate = errors.New("invalid civil date")
var timeNow = time.Now
// Date2JD 日期转儒略日
func Date2JD(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 JDCalc(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
}
// JDCalc 由公历年月日(day 可含小数)计算儒略日 / Julian day from a civil year, month and fractional day.
// 1582 年 10 月 15 日起按格里高利历,之前按儒略历;日期非法时返回 NaN。
// Gregorian from 1582-10-15 onward and Julian before it; an invalid civil date yields NaN.
func JDCalc(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)
}
// GetNowJD 按当前时区的日历字段取儒略日 / Julian day from the current clock's calendar fields.
// 读的是当前时区的年月日时分秒,不做时区归算。
// It reads the calendar fields in the current location without any zone conversion.
func GetNowJD() (nowJD float64) {
now := timeNow()
dayFraction := float64(now.Second())/3600.0/24.0 + float64(now.Minute())/60.0/24.0 + float64(now.Hour())/24.0
nowJD = JDCalc(now.Year(), int(now.Month()), float64(now.Day())+dayFraction)
return
}
// JD2Date 儒略日转 Local 时区的时刻 / Local-zone instant from a Julian day.
// 儒略日的日历字段按 Local 时区解释,1582 年 10 月 15 日之前按儒略历。
// The calendar fields are read in the Local zone, and dates before 1582-10-15 are read in the Julian calendar.
func JD2Date(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))
}
// JD2DateByZone 儒略日转日期
// jd: 儒略日
// tz: 目标时区
// byZone: (true: 传入的儒略日视为目标时区当地时间的儒略日,false: 传入的儒略日视为UTC时间的儒略日)
// 回参:转换后的日期,时区始终为目标时区
func JD2DateByZone(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)
date := time.Date(int(years), time.Month(int(months)), int(days), 0, 0, 0, 0, time.UTC).
Add(time.Duration(int64(1000000000 * tms)))
if !byZone {
return date.In(tz)
}
// 当地 JD 的小数部分是钟表读数,不能按夏令时午夜后的实际时长累加。
year, month, day := date.Date()
hour, minute, second := date.Clock()
return time.Date(year, month, day, hour, minute, second, date.Nanosecond(), tz)
}
+34
View File
@@ -0,0 +1,34 @@
package basic
import (
"math"
"testing"
"time"
)
func TestGetNowJDUsesSingleTimestamp(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 := GetNowJD()
want := Date2JD(first)
if calls != 1 {
t.Fatalf("GetNowJD should read current time once, got %d calls", calls)
}
if math.Float64bits(got) != math.Float64bits(want) {
t.Fatalf("GetNowJD mismatch: got %.15f want %.15f", got, want)
}
}
+28
View File
@@ -0,0 +1,28 @@
package basic
import (
"testing"
"time"
)
func TestLocalJulianDayAcrossDST(t *testing.T) {
for _, name := range []string{"America/New_York", "Australia/Lord_Howe"} {
loc, err := time.LoadLocation(name)
if err != nil {
t.Fatal(err)
}
for _, md := range [][2]int{{3, 8}, {11, 1}, {4, 5}, {10, 4}} {
for _, hour := range []int{0, 3, 12, 23} {
want := time.Date(2026, time.Month(md[0]), md[1], hour, 17, 23, 125000000, loc)
got := JD2DateByZone(Date2JD(want), loc, true)
if delta := got.Sub(want); delta < -100*time.Microsecond || delta > 100*time.Microsecond {
t.Errorf("local %s: got %s, delta=%s", want, got, delta)
}
got = JD2DateByZone(Date2JD(want.UTC()), loc, false)
if delta := got.Sub(want); delta < -100*time.Microsecond || delta > 100*time.Microsecond {
t.Errorf("UTC %s: got %s, delta=%s", want, got, delta)
}
}
}
}
}
+116 -355
View File
@@ -7,431 +7,192 @@ import (
. "b612.me/astro/tools"
)
func JupiterL(JD float64) float64 {
return planet.WherePlanet(4, 0, JD)
func JupiterL(jde float64) float64 {
return planet.WherePlanet(4, 0, jde)
}
func JupiterB(JD float64) float64 {
return planet.WherePlanet(4, 1, JD)
func JupiterB(jde float64) float64 {
return planet.WherePlanet(4, 1, jde)
}
func JupiterR(JD float64) float64 {
return planet.WherePlanet(4, 2, JD)
func JupiterR(jde float64) float64 {
return planet.WherePlanet(4, 2, jde)
}
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(jde float64) float64 {
l := JupiterL(jde)
b := JupiterB(jde)
r := JupiterR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x
}
func AJupiterY(JD float64) float64 {
func AJupiterY(jde 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(jde)
b := JupiterB(jde)
r := JupiterR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
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(jde float64) float64 {
//l := JupiterL(jde)
b := JupiterB(jde)
r := JupiterR(jde)
// el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
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(jde float64) (float64, float64, float64) {
l := JupiterL(jde)
b := JupiterB(jde)
r := JupiterR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
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(jde float64) float64 {
lo, bo := JupiterApparentLoBo(jde)
eps := TrueObliquity(jde)
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(jde float64) float64 {
lo, bo := JupiterApparentLoBo(jde)
eps := TrueObliquity(jde)
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(jde float64) (float64, float64) {
lo, bo := JupiterApparentLoBo(jde)
eps := TrueObliquity(jde)
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 += HJZD(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+=HJZD(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 += HJZD(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(jde float64) float64 {
sunDistance := JupiterR(jde)
earthDistance := EarthJupiterAway(jde)
earthSunDistance := planet.WherePlanet(-1, 2, jde)
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 {
func JupiterHeight(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJde := jde - timezone/24.0
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := JupiterApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := JupiterApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*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)
}
func JupiterAzimuth(jde, lon, lat, timezone float64) float64 {
func JupiterAzimuth(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJde := jde - timezone/24.0
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := JupiterApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := JupiterApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*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(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - JupiterApparentRa(UTC2TT(jd-timezone/24.0))
if hourAngle < 0 {
hourAngle += 360
}
return timeangle
return hourAngle
}
func JupiterCulminationTime(jde, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
func JupiterCulminationTime(localJD, lon, timezone float64) float64 {
// localJD 是本地民用日锚点(当地 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
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-JupiterHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := JupiterHourAngle(localJD, 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 {
break
}
var ok bool
estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 {
hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360
hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001
return hourAngleDelta / hourAngleSlope
})
if !ok {
return math.NaN()
}
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)
}
+283
View File
@@ -0,0 +1,283 @@
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
if !isFiniteFloat(jde) || !isFiniteFloat(degree) {
return math.NaN()
}
daysPerDegree := JUPITER_S_PERIOD / 360
currentDelta := jupiterSunLongitudeDelta(jde, degree, false)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
jde += daysPerDegree * currentDelta
}
estimateJDE := jde
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJDE+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
}
if !converged {
return math.NaN()
}
return TT2UTC(estimateJDE)
}
func jupiterConjunction(jde, degree float64, next uint8) float64 {
//0=last 1=next
if !isFiniteFloat(jde) || !isFiniteFloat(degree) {
return math.NaN()
}
daysPerDegree := JUPITER_S_PERIOD / 360
currentDelta := jupiterSunLongitudeDelta(jde, degree, false)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
jde += daysPerDegree * currentDelta
}
estimateJDE := jde
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDeltaN(prevJDE, degree, true, jupiterEventSearchN)
longitudeSlope := (jupiterSunLongitudeDeltaN(prevJDE+0.000005, degree, true, jupiterEventSearchN) - jupiterSunLongitudeDeltaN(prevJDE-0.000005, degree, true, jupiterEventSearchN)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= jupiterPhaseCoarseTolerance {
converged = true
break
}
}
if !converged {
return math.NaN()
}
converged = false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJDE+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
}
if !converged {
return math.NaN()
}
return TT2UTC(estimateJDE)
}
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 {
if !isFiniteFloat(oppositionJD) {
return math.NaN()
}
oppositionTT := UTC2TT(oppositionJD)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
easternQuadratureUT := jupiterConjunction(oppositionTT, 90, 0)
startTT = UTC2TT(easternQuadratureUT)
} else {
westernQuadratureUT := jupiterConjunction(oppositionTT, 270, 1)
endTT = UTC2TT(westernQuadratureUT)
}
bestJDE := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return jupiterRADerivativeN(jd, stationDerivativeStepDay, jupiterEventSearchN)
}, func(jd float64) float64 {
return jupiterRADerivative(jd, stationDerivativeStepDay)
})
return TT2UTC(bestJDE)
}
func NextJupiterRetrogradeToPrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
date = jupiterRetrogradeAroundOpposition(nextOppositionJD, false)
if !isFiniteFloat(date) || !stationUTQueryAfterOrEqual(date, jde) {
return math.NaN()
}
return date
}
func LastJupiterRetrogradeToPrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
if !isFiniteFloat(lastOppositionJD) {
return math.NaN()
}
previousOppositionJD := jupiterConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
if !isFiniteFloat(previousOppositionJD) {
return math.NaN()
}
date = jupiterRetrogradeAroundOpposition(previousOppositionJD, false)
if !isFiniteFloat(date) || !stationUTQueryBeforeOrEqual(date, jde) {
return math.NaN()
}
return date
}
func NextJupiterProgradeToRetrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
followingOppositionJD := jupiterConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
if !isFiniteFloat(followingOppositionJD) {
return math.NaN()
}
date = jupiterRetrogradeAroundOpposition(followingOppositionJD, true)
if !isFiniteFloat(date) || !stationUTQueryAfterOrEqual(date, jde) {
return math.NaN()
}
return date
}
func LastJupiterProgradeToRetrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
if !isFiniteFloat(lastOppositionJD) {
return math.NaN()
}
date = jupiterRetrogradeAroundOpposition(lastOppositionJD, true)
if !isFiniteFloat(date) || !stationUTQueryBeforeOrEqual(date, jde) {
return math.NaN()
}
return date
}
+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(jde float64) JupiterCentralMeridianInfo {
return JupiterCentralMeridiansN(jde, -1)
}
// JupiterCentralMeridiansN 木星 System I/II/III 中央经线(截断版) / truncated Jupiter System I/II/III central meridians.
func JupiterCentralMeridiansN(jde float64, n int) JupiterCentralMeridianInfo {
observations := jupiterPhysicalObservationsN(jde, n)
physical := JupiterPhysicalN(jde, n)
return JupiterCentralMeridianInfo{
SystemI: observations.SystemI,
SystemII: observations.SystemII,
SystemIII: physical.SubEarthLongitude,
}
}
// JupiterDSDE 木星 DS/DE 行星中心赤纬 / Jupiter planetocentric declinations of Sun and Earth.
func JupiterDSDE(jde float64) (ds, de float64) {
return JupiterDSDEN(jde, -1)
}
// JupiterDSDEN 木星 DS/DE 行星中心赤纬(截断版) / truncated Jupiter planetocentric declinations of Sun and Earth.
func JupiterDSDEN(jde float64, n int) (ds, de float64) {
observations := jupiterPhysicalObservationsN(jde, n)
return observations.DS, observations.DE
}
func jupiterPhysicalObservationsN(jde float64, n int) jupiterPhysicalObservationInfo {
days := jde - 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, jde, n)
earthLat := planet.WherePlanetN(-1, 1, jde, n)
earthRadius := planet.WherePlanetN(-1, 2, jde, 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, jde-lightTimeDays, n)
jupiterLat = planet.WherePlanetN(4, 1, jde-lightTimeDays, n)
jupiterRadius = planet.WherePlanetN(4, 2, jde-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(jde, 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 := UTC2TT(Date2JD(date.UTC()))
got := JupiterCentralMeridians(jd)
assertPlanetPhaseClose(t, "Jupiter."+sample.InputUTC+".CMIII", got.SystemIII, sample.SubEarthLongitude, 0.02)
}
}
func TestJupiterCentralMeridiansNFullMatchesDefault(t *testing.T) {
jd := UTC2TT(Date2JD(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)))
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 := UTC2TT(Date2JD(date.UTC()))
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)
}
}
+796
View File
@@ -0,0 +1,796 @@
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 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
}
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
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
}
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
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 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
}
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
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(Date2JD(queryMid.UTC()), record.Satellite, phenomenonType)
if !event.Valid {
t.Fatalf("%s invalid contact event", record.Label)
}
gotStart := JD2DateByZone(event.Disappearance.Start, time.UTC, false)
gotEnd := JD2DateByZone(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(Date2JD(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
}
}
+645
View File
@@ -0,0 +1,645 @@
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
// signed 是该现象相对木星视面中心线的有符号偏移(木星半径),每次过零对应一次现象;未定义时为 NaN。
signed 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.
//
// jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func LastJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
return event
}
// NextJupiterGalileanPhenomenonEvent 下一次伽利略卫星现象 / next Galilean-satellite event.
//
// jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func NextJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
return event
}
// ClosestJupiterGalileanPhenomenonEvent 最近一次伽利略卫星现象 / closest Galilean-satellite event.
//
// jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
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 event, ok := jupiterGalileanIntervalCandidate(
prevTime, nextTime, prevSample, midSample, nextSample,
jd, satellite, phenomenonType, direction, includeCurrent,
); ok {
return event, true
}
prevTime, prevSample = midTime, midSample
midTime, midSample = nextTime, nextSample
}
return invalidJupiterGalileanPhenomenonEvent(), false
}
// jupiterGalileanIntervalCandidate 在粗扫区间内确认现象:度量局部极小,或有符号偏移过零。
func jupiterGalileanIntervalCandidate(
prevTime, nextTime float64,
prevSample, midSample, nextSample jupiterGalileanMetricSample,
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
direction int,
includeCurrent bool,
) (JupiterGalileanPhenomenonEvent, bool) {
var best JupiterGalileanPhenomenonEvent
found := false
consider := func(event JupiterGalileanPhenomenonEvent) {
if !event.Valid ||
!jupiterGalileanEventMatchesDirection(event.Greatest, jd, direction, includeCurrent) {
return
}
if !found ||
(direction > 0 && event.Greatest < best.Greatest) ||
(direction < 0 && event.Greatest > best.Greatest) {
best, found = event, true
}
}
if isFinite(midSample.metric) &&
midSample.metric <= prevSample.metric &&
midSample.metric <= nextSample.metric {
consider(findJupiterGalileanPhenomenonEventAround(
refineJupiterGalileanMetricMinimum(prevTime, nextTime, satellite, phenomenonType),
satellite, phenomenonType,
))
}
if jupiterGalileanSignedCrosses(prevSample.signed, nextSample.signed) &&
jupiterGalileanNearDisk(prevSample, midSample, nextSample) {
crossing := refineJupiterGalileanSignedCrossing(prevTime, nextTime, satellite, phenomenonType)
// 过零点只说明"靠近木星",只在它已贴近视面时按事件尺度细扫一次。
if sample := jupiterGalileanPhenomenonMetricAt(crossing, satellite, phenomenonType); isFinite(sample.metric) &&
sample.metric <= jupiterGalileanCrossingProbeMetric {
if event, ok := jupiterGalileanFineScanAround(crossing, satellite, phenomenonType); ok {
consider(event)
}
}
}
return best, found
}
const (
// jupiterGalileanCrossingProbeMetric 是细扫门限(椭球度量,1 为视面边缘)。
jupiterGalileanCrossingProbeMetric = 2.5
// jupiterGalileanCrossingProbeDays 是过零点两侧的细扫半宽。
jupiterGalileanCrossingProbeDays = 15.0 / 1440.0
// jupiterGalileanCrossingProbeStepDays 是细扫步长(30 秒),短于最短的擦边事件。
jupiterGalileanCrossingProbeStepDays = 0.5 / 1440.0
)
// jupiterGalileanNearDisk 报告区间内是否已有贴近木星视面的采样点。
func jupiterGalileanNearDisk(prevSample, midSample, nextSample jupiterGalileanMetricSample) bool {
for _, metric := range [3]float64{prevSample.metric, midSample.metric, nextSample.metric} {
if isFinite(metric) && metric <= jupiterGalileanCrossingProbeMetric {
return true
}
}
return false
}
// jupiterGalileanFineScanAround 在过零点邻域按事件尺度取样,命中后交给事件求解。
func jupiterGalileanFineScanAround(
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) (JupiterGalileanPhenomenonEvent, bool) {
// 从过零点向两侧交替外扩,通常几步内就能落进事件窗口。
steps := int(math.Round(jupiterGalileanCrossingProbeDays / jupiterGalileanCrossingProbeStepDays))
for step := 0; step <= steps; step++ {
for _, sign := range [2]float64{1, -1} {
if step == 0 && sign < 0 {
continue
}
candidate := jd + sign*float64(step)*jupiterGalileanCrossingProbeStepDays
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType).active {
continue
}
if event := findJupiterGalileanPhenomenonEventAround(candidate, satellite, phenomenonType); event.Valid {
return event, true
}
}
}
return JupiterGalileanPhenomenonEvent{}, false
}
// jupiterGalileanSignedCrosses 判断两次采样的有符号偏移是否变号(端点为 0 也算)。
func jupiterGalileanSignedCrosses(first, second float64) bool {
if !isFinite(first) || !isFinite(second) {
return false
}
if first == 0 || second == 0 {
return true
}
return (first < 0) != (second < 0)
}
// refineJupiterGalileanSignedCrossing 用二分把过零时刻收敛到事件容差内。
func refineJupiterGalileanSignedCrossing(
left, right float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
leftValue := jupiterGalileanPhenomenonMetricAt(left, satellite, phenomenonType).signed
rightValue := jupiterGalileanPhenomenonMetricAt(right, satellite, phenomenonType).signed
if !isFinite(leftValue) || !isFinite(rightValue) || (leftValue < 0) == (rightValue < 0) {
return (left + right) / 2
}
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
middle := (left + right) / 2
value := jupiterGalileanPhenomenonMetricAt(middle, satellite, phenomenonType).signed
if !isFinite(value) {
return middle
}
if (value < 0) == (leftValue < 0) {
left, leftValue = middle, value
} else {
right, rightValue = middle, value
}
}
return (left + right) / 2
}
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),
signed: math.NaN(),
phenomenon: invalidJupiterGalileanPhenomenon(),
}
}
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
if context.jupiterDistance == 0 {
return jupiterGalileanMetricSample{
metric: math.Inf(1),
signed: math.NaN(),
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, signed: xEarth, phenomenon: phenomenon,
}
case JupiterGalileanOccultation:
metric := earthMetric
if observation.InFrontOfJupiter {
metric += 4
}
return jupiterGalileanMetricSample{
active: phenomenon.Occultation, metric: metric, signed: xEarth, phenomenon: phenomenon,
}
case JupiterGalileanEclipse:
metric := sunMetric
if zSunAU <= 0 {
metric += 4
}
return jupiterGalileanMetricSample{
active: phenomenon.Eclipse, metric: metric, signed: xSun, phenomenon: phenomenon,
}
case JupiterGalileanShadowTransit:
metric := shadowMetric
if shadowPoint.hasIntersection && !shadowPoint.visible {
metric += 4
}
signed := math.NaN()
if shadowPoint.hasIntersection {
signed = shadowPoint.xJupiterRadii
}
return jupiterGalileanMetricSample{
active: phenomenon.ShadowTransit, metric: metric, signed: signed, phenomenon: phenomenon,
}
default:
return jupiterGalileanMetricSample{
metric: math.Inf(1), signed: math.NaN(), 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(),
}
}
+181
View File
@@ -0,0 +1,181 @@
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(Date2JD(queryBefore.UTC()), record.Satellite, phenomenonType)
last := LastJupiterGalileanPhenomenonEvent(Date2JD(queryAfter.UTC()), record.Satellite, phenomenonType)
closest := ClosestJupiterGalileanPhenomenonEvent(Date2JD(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 := JD2DateByZone(event.Start, time.UTC, false)
gotEnd := JD2DateByZone(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
}
func TestJupiterGalileanSearchFindsShortGrazingTransit(t *testing.T) {
// 2463144 附近 Callisto 有一次擦边凌日:Start 2463143.9993 / Greatest 2463144.0015 /
// End 2463144.0038,全场只有 6.4 分钟,整段落在 2 小时粗采样之间。旧实现看不到度量
// 凹陷,会把 16.85 天后的下一次凌日当成 "next"(Valid 仍为 true,属静默跳事件)。
// Callisto has a grazing transit near 2463144: Start 2463143.9993, Greatest
// 2463144.0015, End 2463144.0038 — only 6.4 minutes, entirely between two two-hour
// coarse samples. The old search saw no metric dip and reported the next transit 16.85
// days later as "next", still with Valid=true (a silent skip).
closest := ClosestJupiterGalileanPhenomenonEvent(2463144.0, 4, JupiterGalileanTransit)
if !closest.Valid {
t.Fatal("no Callisto transit found at 2463144")
}
if duration := closest.End - closest.Start; duration > 0.01 {
t.Fatalf("fixture changed: transit duration %.4f d, want the 6.4-minute grazing event", duration)
}
next := NextJupiterGalileanPhenomenonEvent(2463143.9646, 4, JupiterGalileanTransit)
if !next.Valid {
t.Fatal("NextJupiterGalileanPhenomenonEvent returned nothing")
}
if offset := next.Greatest - 2463143.9646; offset > 0.1 {
t.Fatalf("next transit is %.3f d away, want the 53-minute one (the search skipped it)", offset)
}
if math.Abs(next.Greatest-closest.Greatest) > 1e-5 {
t.Fatalf("next greatest %.9f, want the same event as closest %.9f", next.Greatest, closest.Greatest)
}
last := LastJupiterGalileanPhenomenonEvent(2463144.05, 4, JupiterGalileanTransit)
if !last.Valid {
t.Fatal("LastJupiterGalileanPhenomenonEvent returned nothing")
}
if math.Abs(last.Greatest-closest.Greatest) > 1e-5 {
t.Fatalf("last greatest %.9f, want the same event as closest %.9f", last.Greatest, closest.Greatest)
}
}
+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(jde float64, satellite int) JupiterGalileanPhenomenon {
if satellite < 1 || satellite > 4 || !isFinite(jde) {
return invalidJupiterGalileanPhenomenon()
}
context := newJupiterGalileanObservationContext(jde)
return context.phenomenonForSatellite(satellite - 1)
}
// JupiterGalileanSatellitePhenomena 四颗伽利略卫星瞬时现象 / instantaneous phenomena of the four Galilean satellites.
func JupiterGalileanSatellitePhenomena(jde float64) [4]JupiterGalileanPhenomenon {
var phenomena [4]JupiterGalileanPhenomenon
context := newJupiterGalileanObservationContext(jde)
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,
}
}
+439
View File
@@ -0,0 +1,439 @@
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 (convert a civil query with UTC2TT(jd)). 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(jde float64, satellite int) JupiterGalileanObservation {
if satellite < 1 || satellite > 4 || !isFinite(jde) {
return invalidJupiterGalileanObservation()
}
context := newJupiterGalileanObservationContext(jde)
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(jde float64) [4]JupiterGalileanObservation {
var observations [4]JupiterGalileanObservation
context := newJupiterGalileanObservationContext(jde)
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(jde float64) jupiterGalileanObservationContext {
context := jupiterGalileanObservationContext{jd: jde}
if !isFinite(jde) {
return context
}
context.earthHelioJ2000 = rotateEclipticToEquatorial(earthHeliocentricVectorJ2000(jde), orbitJ2000Obliquity)
context.jupiterGeoJ2000, context.jupiterLightTime = jupiterAstrometricGeocentricVectorJ2000(jde, context.earthHelioJ2000)
context.targetJD = jde - 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++ {
targetJDE := jd - lightTime
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(targetJDE), orbitJ2000Obliquity)
state = jupiterGalileanSatelliteStateAtET(targetJDE-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(jde float64) Vector3 {
return eclipticVectorAtReferenceEpoch(
eclipticCartesian(
planet.WherePlanet(4, 0, jde),
planet.WherePlanet(4, 1, jde),
planet.WherePlanet(4, 2, jde),
),
jde,
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
invalid := func() [6]float64 {
nan := math.NaN()
return [6]float64{nan, nan, nan, nan, nan, nan}
}
if !isFiniteFloat(mu) || !isFiniteFloat(k) || !isFiniteFloat(h) || !isFiniteFloat(q) || !isFiniteFloat(p) || !isFiniteFloat(a) || !isFiniteFloat(al) || a == 0 {
return invalid()
}
an := math.Sqrt(mu / math.Pow(a, 3))
ee := al + k*math.Sin(al) - h*math.Cos(al)
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
ce := math.Cos(ee)
se := math.Sin(ee)
de := (al - ee + k*se - h*ce) / (1 - k*ce - h*se)
if !isFiniteFloat(de) {
return invalid()
}
ee += de
if math.Abs(de) < 1e-12 {
converged = true
break
}
}
if !converged {
return invalid()
}
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)))
}
}
+244
View File
@@ -0,0 +1,244 @@
package basic
import "math"
// localEphemerisVectorNode is a pair of Cartesian ephemeris samples at one
// TT. Cartesian interpolation avoids right-ascension wraparound at 0/360.
type localEphemerisVectorNode struct {
tt float64
first, next [3]float64
}
const (
solarLocalEphemerisNodeCount = 13
solarLocalEphemerisStepDays = 1.0 / 24.0
occultationLocalEphemerisNodeCount = 49
occultationLocalEphemerisStepDays = 2.0 / 24.0
)
func interpolateLocalEphemerisVectors(
nodes []localEphemerisVectorNode,
tt float64,
) ([3]float64, [3]float64, bool) {
const interpolationPoints = 6
if len(nodes) < interpolationPoints || !finite(tt) {
return [3]float64{}, [3]float64{}, false
}
step := nodes[1].tt - nodes[0].tt
if !finite(step) || step <= 0 {
return [3]float64{}, [3]float64{}, false
}
u := (tt - nodes[0].tt) / step
if u < 0 || u > float64(len(nodes)-1) {
return [3]float64{}, [3]float64{}, false
}
start := int(math.Floor(u)) - 2
if start < 0 {
start = 0
}
if start > len(nodes)-interpolationPoints {
start = len(nodes) - interpolationPoints
}
var weights [interpolationPoints]float64
for point := 0; point < interpolationPoints; point++ {
x := float64(start + point)
weight := 1.0
for other := 0; other < interpolationPoints; other++ {
if other == point {
continue
}
xOther := float64(start + other)
weight *= (u - xOther) / (x - xOther)
}
weights[point] = weight
}
var first, next [3]float64
for coordinate := 0; coordinate < 3; coordinate++ {
for point := 0; point < interpolationPoints; point++ {
first[coordinate] += weights[point] * nodes[start+point].first[coordinate]
next[coordinate] += weights[point] * nodes[start+point].next[coordinate]
}
}
return first, next, finiteVector3(first) && finiteVector3(next)
}
func finiteVector3(value [3]float64) bool {
return finite(value[0]) && finite(value[1]) && finite(value[2])
}
func occultationPathVectorRaDec(vector occultationPathVector) (float64, float64, float64, bool) {
distance := occultationPathNorm(vector)
if !finite(distance) || distance <= 0 {
return 0, 0, 0, false
}
// atan2 给 (−180,180],统一到 [0,360):与精确分支(LoBoToRaDec、starMeanToApparentRaDec)
// 和 occultationRiseSetBodyFromVector 的 normalizeRA 一致;下游只按周期量使用,数值不变。
ra := normalizeRA(math.Atan2(vector.y, vector.x) / rad)
dec := math.Asin(math.Max(-1, math.Min(1, vector.z/distance))) / rad
return ra, dec, distance, finite(ra) && finite(dec)
}
type solarEclipseLocalEphemeris struct {
nodes []localEphemerisVectorNode
}
func newSolarEclipseLocalEphemeris(center float64) *solarEclipseLocalEphemeris {
half := solarLocalEphemerisNodeCount / 2
nodes := make([]localEphemerisVectorNode, solarLocalEphemerisNodeCount)
for index := range nodes {
tt := center + float64(index-half)*solarLocalEphemerisStepDays
sun, moon := solarEclipseSunMoonEquatorial(tt)
nodes[index] = localEphemerisVectorNode{
tt: tt,
first: solarEclipseLLRToXYZ(sun[0], sun[1], sun[2]),
next: solarEclipseLLRToXYZ(moon[0], moon[1], moon[2]),
}
}
return &solarEclipseLocalEphemeris{nodes: nodes}
}
func (ephemeris *solarEclipseLocalEphemeris) equatorialAt(tt float64) ([3]float64, [3]float64, bool) {
var empty [3]float64
if ephemeris == nil {
return empty, empty, false
}
sunXYZ, moonXYZ, ok := interpolateLocalEphemerisVectors(ephemeris.nodes, tt)
if !ok {
return empty, empty, false
}
return solarEclipseXYZToLLR(sunXYZ[0], sunXYZ[1], sunXYZ[2]),
solarEclipseXYZToLLR(moonXYZ[0], moonXYZ[1], moonXYZ[2]), true
}
type starOccultationLocalEphemeris struct {
star StarCoordinate
nodes []localEphemerisVectorNode
dense bool
// distanceKM 是中心时刻的当日距离;hasDistance 为假表示恒星距离未知,几何按无穷远处理。
distanceKM float64
hasDistance bool
}
func newStarOccultationLocalEphemeris(center float64, star StarCoordinate) *starOccultationLocalEphemeris {
half := occultationLocalEphemerisNodeCount / 2
nodes := make([]localEphemerisVectorNode, occultationLocalEphemerisNodeCount)
for index := range nodes {
tt := center + float64(index-half)*occultationLocalEphemerisStepDays
state := starOccultationEphemerisStateAt(tt, star)
moon := occultationPathRaDecVector(state.moonRA, state.moonDec, state.moonDistanceKM)
targetDistance := state.starDistanceKM
if targetDistance <= 0 {
// 恒星距离未知时只需方向:按单位球方向装配,几何只用归一化后的矢量。
targetDistance = 1
}
target := occultationPathRaDecVector(state.starRA, state.starDec, targetDistance)
nodes[index] = localEphemerisVectorNode{
tt: tt,
first: [3]float64{moon.x, moon.y, moon.z},
next: [3]float64{target.x, target.y, target.z},
}
}
return newStarOccultationLocalEphemerisFromNodes(center, star, nodes, false)
}
// newStarOccultationLocalEphemerisFromNodes 装配局部星历:距离必须与节点同源,密集分支同样要带上。
func newStarOccultationLocalEphemerisFromNodes(center float64, star StarCoordinate, nodes []localEphemerisVectorNode, dense bool) *starOccultationLocalEphemeris {
_, _, distanceAU := starApparentRaDecDistanceGeocentric(center, star)
return &starOccultationLocalEphemeris{
star: star, nodes: nodes, dense: dense,
distanceKM: distanceAU * occultationPathAstronomicalUnitKM, hasDistance: distanceAU > 0,
}
}
func (ephemeris *starOccultationLocalEphemeris) stateAt(tt float64) (starOccultationEphemerisState, bool) {
moonXYZ, targetXYZ, ok := ephemeris.vectorsAt(tt)
if !ok {
return starOccultationEphemerisState{}, false
}
moonRA, moonDec, moonDistance, moonOK := occultationPathVectorRaDec(occultationPathVector{
x: moonXYZ[0], y: moonXYZ[1], z: moonXYZ[2],
})
targetRA, targetDec, targetDistance, targetOK := occultationPathVectorRaDec(occultationPathVector{
x: targetXYZ[0], y: targetXYZ[1], z: targetXYZ[2],
})
if !moonOK || !targetOK {
return starOccultationEphemerisState{}, false
}
// 距离取插值矢量自身的模长,才与同一次插值给出的方向同源;距离未知时按 0 上报。
starDistanceKM := 0.0
if ephemeris.hasDistance {
starDistanceKM = targetDistance
}
return starOccultationEphemerisState{
moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistance,
starRA: targetRA, starDec: targetDec, starDistanceKM: starDistanceKM,
valid: true,
}, true
}
func (ephemeris *starOccultationLocalEphemeris) vectorsAt(tt float64) ([3]float64, [3]float64, bool) {
if ephemeris == nil {
return [3]float64{}, [3]float64{}, false
}
if ephemeris.dense {
return interpolateDenseOccultationVectors(ephemeris.nodes, tt)
}
return interpolateLocalEphemerisVectors(ephemeris.nodes, tt)
}
func (ephemeris *starOccultationLocalEphemeris) starDistanceKM() float64 {
return ephemeris.distanceKM
}
type planetOccultationLocalEphemeris struct {
nodes []localEphemerisVectorNode
dense bool
}
func newPlanetOccultationLocalEphemeris(center float64, config planetOccultationConfig) *planetOccultationLocalEphemeris {
half := occultationLocalEphemerisNodeCount / 2
nodes := make([]localEphemerisVectorNode, occultationLocalEphemerisNodeCount)
for index := range nodes {
tt := center + float64(index-half)*occultationLocalEphemerisStepDays
state := planetOccultationEphemerisStateAt(tt, config)
moon := occultationPathRaDecVector(state.moonRA, state.moonDec, state.moonDistanceKM)
target := occultationPathRaDecVector(state.planetRA, state.planetDec, state.planetDistanceKM)
nodes[index] = localEphemerisVectorNode{
tt: tt,
first: [3]float64{moon.x, moon.y, moon.z},
next: [3]float64{target.x, target.y, target.z},
}
}
return &planetOccultationLocalEphemeris{nodes: nodes}
}
func (ephemeris *planetOccultationLocalEphemeris) stateAt(tt float64) (planetOccultationEphemerisState, bool) {
moonXYZ, targetXYZ, ok := ephemeris.vectorsAt(tt)
if !ok {
return planetOccultationEphemerisState{}, false
}
moonRA, moonDec, moonDistance, moonOK := occultationPathVectorRaDec(occultationPathVector{
x: moonXYZ[0], y: moonXYZ[1], z: moonXYZ[2],
})
targetRA, targetDec, planetDistance, targetOK := occultationPathVectorRaDec(occultationPathVector{
x: targetXYZ[0], y: targetXYZ[1], z: targetXYZ[2],
})
if !moonOK || !targetOK {
return planetOccultationEphemerisState{}, false
}
return planetOccultationEphemerisState{
moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistance,
planetRA: targetRA, planetDec: targetDec, planetDistanceKM: planetDistance,
valid: true,
}, true
}
func (ephemeris *planetOccultationLocalEphemeris) vectorsAt(tt float64) ([3]float64, [3]float64, bool) {
if ephemeris == nil {
return [3]float64{}, [3]float64{}, false
}
if ephemeris.dense {
return interpolateDenseOccultationVectors(ephemeris.nodes, tt)
}
return interpolateLocalEphemerisVectors(ephemeris.nodes, tt)
}
+157
View File
@@ -0,0 +1,157 @@
package basic
import (
"math"
"testing"
"time"
)
func TestSolarEclipseLocalEphemerisBoundedError(t *testing.T) {
events := []struct {
name string
seed float64
}{
{"2010-01-15", JDCalc(2010, 1, 15)},
{"2014-04-29", JDCalc(2014, 4, 29)},
{"2023-04-20", JDCalc(2023, 4, 20)},
{"2031-05-21", JDCalc(2031, 5, 21)},
{"2309-06-09", JDCalc(2309, 6, 9)},
}
for _, event := range events {
t.Run(event.name, func(t *testing.T) {
center := CalcMoonSHByJDE(event.seed, 0)
ephemeris := newSolarEclipseLocalEphemeris(center)
for offset := -4.5; offset <= 4.5; offset += 0.25 {
jd := center + offset/24
approxSun, approxMoon, ok := ephemeris.equatorialAt(jd)
if !ok {
t.Fatalf("interpolator rejected in-window time %.3fh", offset)
}
exactSun, exactMoon := solarEclipseSunMoonEquatorial(jd)
for name, pair := range map[string][2][3]float64{
"sun": {solarEclipseLLRToXYZ(approxSun[0], approxSun[1], approxSun[2]), solarEclipseLLRToXYZ(exactSun[0], exactSun[1], exactSun[2])},
"moon": {solarEclipseLLRToXYZ(approxMoon[0], approxMoon[1], approxMoon[2]), solarEclipseLLRToXYZ(exactMoon[0], exactMoon[1], exactMoon[2])},
} {
if errorKM := vectorDifferenceKM(pair[0], pair[1]); errorKM > 1 {
t.Fatalf("%s interpolation error at %.3fh = %.6f km", name, offset, errorKM)
}
}
}
if _, _, ok := ephemeris.equatorialAt(center + 6.1/24); ok {
t.Fatal("interpolator should reject times outside its bounded window")
}
})
}
}
func TestOccultationLocalEphemerisBoundedError(t *testing.T) {
star := hr4799OccultationCoordinateForTest()
starTT := occultationTimeToTT(time.Date(2025, 6, 5, 20, 0, 0, 0, time.UTC))
starEphemeris := newStarOccultationLocalEphemeris(starTT, star)
for offset := -4.5; offset <= 4.5; offset += 0.25 {
jd := starTT + offset/24
got, ok := starEphemeris.stateAt(jd)
if !ok {
t.Fatalf("star interpolator rejected in-window time %.3fh", offset)
}
want := starOccultationEphemerisStateAt(jd, star)
if difference := angularSeparationDegrees(got.moonRA, got.moonDec, want.moonRA, want.moonDec); difference > 1e-5 {
t.Fatalf("moon direction interpolation error at %.3fh = %.9f deg", offset, difference)
}
if difference := angularSeparationDegrees(got.starRA, got.starDec, want.starRA, want.starDec); difference > 1e-5 {
t.Fatalf("star direction interpolation error at %.3fh = %.9f deg", offset, difference)
}
}
config, ok := planetOccultationConfigFor(OccultationSaturn)
if !ok {
t.Fatal("Saturn occultation config is unavailable")
}
planetTT := occultationTimeToTT(time.Date(2024, 8, 21, 2, 40, 0, 0, time.UTC))
planetEphemeris := newPlanetOccultationLocalEphemeris(planetTT, config)
for offset := -4.5; offset <= 4.5; offset += 0.25 {
jd := planetTT + offset/24
got, ok := planetEphemeris.stateAt(jd)
if !ok {
t.Fatalf("planet interpolator rejected in-window time %.3fh", offset)
}
want := planetOccultationEphemerisStateAt(jd, config)
if difference := angularSeparationDegrees(got.moonRA, got.moonDec, want.moonRA, want.moonDec); difference > 1e-5 {
t.Fatalf("moon direction interpolation error at %.3fh = %.9f deg", offset, difference)
}
if difference := angularSeparationDegrees(got.planetRA, got.planetDec, want.planetRA, want.planetDec); difference > 1e-5 {
t.Fatalf("planet direction interpolation error at %.3fh = %.9f deg", offset, difference)
}
if math.Abs(got.planetDistanceKM-want.planetDistanceKM) > 100 {
t.Fatalf("planet distance interpolation error at %.3fh = %.6f km", offset, math.Abs(got.planetDistanceKM-want.planetDistanceKM))
}
}
}
func TestOccultationLocalEphemerisFullWindowBoundedError(t *testing.T) {
star := hr4799OccultationCoordinateForTest()
starTT := occultationTimeToTT(time.Date(2025, 6, 5, 20, 0, 0, 0, time.UTC))
starEphemeris := newStarOccultationLocalEphemeris(starTT, star)
config, ok := planetOccultationConfigFor(OccultationSaturn)
if !ok {
t.Fatal("Saturn occultation config is unavailable")
}
planetTT := occultationTimeToTT(time.Date(2024, 8, 21, 2, 40, 0, 0, time.UTC))
planetEphemeris := newPlanetOccultationLocalEphemeris(planetTT, config)
for _, offset := range []float64{-47.5, -40, -24, 24, 40, 47.5} {
starJD := starTT + offset/24
gotStar, starOK := starEphemeris.stateAt(starJD)
wantStar := starOccultationEphemerisStateAt(starJD, star)
if !starOK {
t.Fatalf("star interpolator rejected in-window time %.3fh", offset)
}
if difference := angularSeparationDegrees(gotStar.moonRA, gotStar.moonDec, wantStar.moonRA, wantStar.moonDec); difference > 1e-5 {
t.Fatalf("star-event moon direction error at %.3fh = %.9f deg", offset, difference)
}
if difference := angularSeparationDegrees(gotStar.starRA, gotStar.starDec, wantStar.starRA, wantStar.starDec); difference > 1e-5 {
t.Fatalf("star direction error at %.3fh = %.9f deg", offset, difference)
}
planetJD := planetTT + offset/24
gotPlanet, planetOK := planetEphemeris.stateAt(planetJD)
wantPlanet := planetOccultationEphemerisStateAt(planetJD, config)
if !planetOK {
t.Fatalf("planet interpolator rejected in-window time %.3fh", offset)
}
if difference := angularSeparationDegrees(gotPlanet.moonRA, gotPlanet.moonDec, wantPlanet.moonRA, wantPlanet.moonDec); difference > 1e-5 {
t.Fatalf("planet-event moon direction error at %.3fh = %.9f deg", offset, difference)
}
if difference := angularSeparationDegrees(gotPlanet.planetRA, gotPlanet.planetDec, wantPlanet.planetRA, wantPlanet.planetDec); difference > 1e-5 {
t.Fatalf("planet direction error at %.3fh = %.9f deg", offset, difference)
}
if difference := math.Abs(gotPlanet.planetDistanceKM - wantPlanet.planetDistanceKM); difference > 100 {
t.Fatalf("planet distance error at %.3fh = %.6f km", offset, difference)
}
}
}
func vectorDifferenceKM(a, b [3]float64) float64 {
return math.Sqrt((a[0]-b[0])*(a[0]-b[0]) + (a[1]-b[1])*(a[1]-b[1]) + (a[2]-b[2])*(a[2]-b[2]))
}
// 矢量回读的赤经必须落在 [0,360):atan2 给 (−180,180],而精确分支
// (LoBoToRaDec、starMeanToApparentRaDec)与 rise/set 的矢量回读都在 [0,360),
// 不归一会让同一时刻的密集/精确状态相差 360 度,误导比较与日志。
func TestOccultationPathVectorRaDecKeepsNormalizedRA(t *testing.T) {
for _, want := range []float64{0.5, 90, 189.1975, 270, 359.9} {
vector := occultationPathRaDecVector(want, -5.8, 2.5)
got, dec, distance, ok := occultationPathVectorRaDec(vector)
if !ok {
t.Fatalf("RA %.4f 的回读失败", want)
}
if math.Abs(got-want) > 1e-9 {
t.Fatalf("RA %.4f 回读为 %.4f,want [0,360) 内的同一读数", want, got)
}
if math.Abs(dec+5.8) > 1e-9 || math.Abs(distance-2.5) > 1e-12 {
t.Fatalf("RA %.4f 的赤纬/距离 = %.6f / %.6f,want -5.8 / 2.5", want, dec, distance)
}
}
if _, _, _, ok := occultationPathVectorRaDec(occultationPathVector{}); ok {
t.Fatal("零矢量应判为无效")
}
}
+393
View File
@@ -0,0 +1,393 @@
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
// ShadowModel 是本次使用的影半径模型,决定影半径与食分的具体数值。
// ShadowModel is the shadow-radius model used, which fixes the shadow radii and magnitudes.
ShadowModel LunarEclipseShadowModel
// Maximum 是食甚时刻;即使最终没有月食,也会返回该次望月附近
// “月面中心最接近地影中心”的几何极值时刻。
Maximum float64
// Magnitude 是本影食分。纯半影月食时可为负值;无月食时为 0。
Magnitude float64
// PenumbralMagnitude 是半影食分。无半影接触时为 0。
PenumbralMagnitude float64
// MinimumDistance 是食甚时月心到地影中心的最小角距离,单位为弧度。
MinimumDistance float64
// Contact times:
// PenumbralStart / PenumbralEnd: 半影食始 / 半影食终
// PartialStart / PartialEnd: 初亏 / 复圆
// TotalStart / TotalEnd: 食既 / 生光
//
// 该阶段不发生时为 NaN(0 是 −4713-11-24 的真实时刻);判断阶段是否存在一律用 Has*。
// A contact is NaN when that phase does not occur (JD 0 is the real instant −4713-11-24);
// decide by the Has* flags, never by comparing a contact against zero.
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
}
// LunarEclipseShadowModel 标识月食用的是哪套影半径模型。
// LunarEclipseShadowModel identifies which shadow-radius model produced a result.
type LunarEclipseShadowModel int
const (
// LunarEclipseShadowModelDanjon 是 Danjon 影半径模型。
LunarEclipseShadowModelDanjon LunarEclipseShadowModel = iota
// LunarEclipseShadowModelChauvenet 是 Chauvenet 影半径模型。
LunarEclipseShadowModelChauvenet
)
type lunarEclipseShadowModel int
const (
lunarEclipseShadowDanjon lunarEclipseShadowModel = iota
lunarEclipseShadowChauvenet
)
const (
lunarEarthEquatorialRadiusKM = 6378.1366
lunarAstronomicalUnitKM = 1.49597870691e8
// 沿用月食常量:
// - 0.2725076 用于月亮视半径和半影几何
// - 太阳视半径与日食共用标准档常量,太阳视差用常用角秒常量 8.794
lunarMoonRadiusRatio = 0.2725076
lunarMoonRadiusScale = lunarMoonRadiusRatio * lunarEarthEquatorialRadiusKM * 1.0000036
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)
// 未发生的阶段保持 NaN:Has* 是权威判据,时刻字段不能拿 0 当哨兵。
model := LunarEclipseShadowModelDanjon
if shadowModel == lunarEclipseShadowChauvenet {
model = LunarEclipseShadowModelChauvenet
}
result := LunarEclipseResult{
Type: LunarEclipseNone,
ShadowModel: model,
Maximum: maximumJDE,
MinimumDistance: minimumDistance,
PenumbralMagnitude: (state.moonRadiusRad + state.penumbraRadiusRad - minimumDistance) / (2 * state.moonRadiusRad),
PenumbralStart: math.NaN(),
PenumbralEnd: math.NaN(),
PartialStart: math.NaN(),
PartialEnd: math.NaN(),
TotalStart: math.NaN(),
TotalEnd: math.NaN(),
}
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 math.NaN()
}
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
}
// lunarEclipsePlaneState 只需要月心相对地影中心的二维坐标(穿影图采样点用,跳过半径所需的距离项)。
type lunarEclipsePlaneState struct {
jde float64
x float64
y float64
}
func lunarEclipsePlaneStateAt(jde float64) lunarEclipsePlaneState {
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)
return lunarEclipsePlaneState{
jde: jde,
x: normalizeRadians(moonLongitude+math.Pi-sunLongitude) * math.Cos((moonLatitude-sunLatitude)/2),
y: moonLatitude + sunLatitude,
}
}
// computeLunarShadowState 计算某一力学时刻下,月心相对地影中心的二维几何状态。
//
// 所有内部角量统一使用弧度。影半径模型允许在 Danjon 与 Chauvenet 之间切换,
// 其余月心轨迹与几何求交框架保持一致。
func computeLunarShadowState(jde float64, shadowModel lunarEclipseShadowModel) lunarShadowState {
plane := lunarEclipsePlaneStateAt(jde)
moonDistanceKM := HMoonAway(jde)
sunDistanceAU := EarthAway(jde)
moonRadiusArcsec := lunarMoonRadiusScale * lunarArcsecPerRadian / moonDistanceKM
earthParallaxArcsec := lunarEarthEquatorialRadiusKM / moonDistanceKM * lunarArcsecPerRadian
solarRadiusArcsec := eclipseSunRadiusStandardArcsec / sunDistanceAU
solarParallaxArcsec := lunarSolarParallaxArcsec / sunDistanceAU
umbraRadiusArcsec, penumbraRadiusArcsec := lunarEclipseShadowRadiiArcsec(
earthParallaxArcsec,
solarRadiusArcsec,
solarParallaxArcsec,
shadowModel,
)
return lunarShadowState{
jde: plane.jde,
x: plane.x,
y: plane.y,
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
}
+98
View File
@@ -0,0 +1,98 @@
package basic
import (
"math"
"testing"
)
// 未发生阶段的接触时刻必须是 NaN:JD 0 是 −4713-11-24 的真实时刻,不能当“无此阶段”的哨兵。
func TestLunarEclipseAbsentPhasesAreNaN(t *testing.T) {
eclipse := LunarEclipse(2458860.0)
if eclipse.Type != LunarEclipsePenumbral {
t.Fatalf("type = %s, want %s", eclipse.Type, LunarEclipsePenumbral)
}
if !eclipse.HasPenumbral || eclipse.HasPartial || eclipse.HasTotal {
t.Fatalf("unexpected flags: %+v", eclipse)
}
for name, value := range map[string]float64{
"PartialStart": eclipse.PartialStart,
"PartialEnd": eclipse.PartialEnd,
"TotalStart": eclipse.TotalStart,
"TotalEnd": eclipse.TotalEnd,
} {
if !math.IsNaN(value) {
t.Fatalf("%s = %.12f, want NaN", name, value)
}
}
for name, value := range map[string]float64{
"PenumbralStart": eclipse.PenumbralStart,
"PenumbralEnd": eclipse.PenumbralEnd,
"Maximum": eclipse.Maximum,
} {
if !isFiniteFloat(value) {
t.Fatalf("%s = %v, want a finite contact", name, value)
}
}
}
func TestLunarEclipseWithoutEclipseHasNoContacts(t *testing.T) {
// 2025-01-13 的望月没有月食(半影食分 < 0)。
eclipse := LunarEclipse(JDCalc(2025, 1, 13))
if eclipse.Type != LunarEclipseNone {
t.Fatalf("type = %s, want %s", eclipse.Type, LunarEclipseNone)
}
if eclipse.HasPenumbral || eclipse.HasPartial || eclipse.HasTotal {
t.Fatalf("unexpected flags: %+v", eclipse)
}
for name, value := range map[string]float64{
"PenumbralStart": eclipse.PenumbralStart,
"PenumbralEnd": eclipse.PenumbralEnd,
"PartialStart": eclipse.PartialStart,
"PartialEnd": eclipse.PartialEnd,
"TotalStart": eclipse.TotalStart,
"TotalEnd": eclipse.TotalEnd,
} {
if !math.IsNaN(value) {
t.Fatalf("%s = %.12f, want NaN", name, value)
}
}
if !isFiniteFloat(eclipse.Maximum) {
t.Fatalf("Maximum = %v, want the geometric extremum", eclipse.Maximum)
}
}
func TestLunarEclipseTotalKeepsAllContactsFinite(t *testing.T) {
eclipse := LunarEclipse(JDCalc(2025, 3, 14))
if eclipse.Type != LunarEclipseTotal || !eclipse.HasPenumbral || !eclipse.HasPartial || !eclipse.HasTotal {
t.Fatalf("unexpected result: %+v", eclipse)
}
contacts := []float64{
eclipse.PenumbralStart, eclipse.PartialStart, eclipse.TotalStart,
eclipse.Maximum,
eclipse.TotalEnd, eclipse.PartialEnd, eclipse.PenumbralEnd,
}
for i, value := range contacts {
if !isFiniteFloat(value) {
t.Fatalf("contact %d = %v, want finite", i, value)
}
if i > 0 && value <= contacts[i-1] {
t.Fatalf("contacts not increasing at %d: %v", i, contacts)
}
}
}
func TestLunarEclipseDiagramSkipsAbsentPhases(t *testing.T) {
diagram := LunarEclipseDiagram(2458860.0, LunarEclipseDiagramOptions{})
if len(diagram.Points) == 0 {
t.Fatalf("penumbral eclipse diagram should still have path points")
}
for _, point := range diagram.Points {
if !isFiniteFloat(point.JDE) || !isFiniteFloat(point.X) || !isFiniteFloat(point.Y) {
t.Fatalf("non-finite diagram point: %+v", point)
}
}
if empty := LunarEclipseDiagram(JDCalc(2025, 1, 13), LunarEclipseDiagramOptions{}); len(empty.Points) != 0 {
t.Fatalf("eclipse-free diagram should have no points, got %d", len(empty.Points))
}
}
+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 {
plane := lunarEclipsePlaneStateAt(item.jde)
result.Points = append(result.Points, LunarEclipseDiagramPoint{
JDE: item.jde,
X: plane.x / maximumState.moonRadiusRad,
Y: plane.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 !isFiniteFloat(startJDE) || !isFiniteFloat(endJDE) || 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 !isFiniteFloat(item.jde) {
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
}
}
+69
View File
@@ -0,0 +1,69 @@
package basic
import "math"
// LunarEclipseShadowGeometry 是食甚时刻的地影几何。
// 注意单位口径:Gamma 用地球赤道半径,而两个影半径用度——后者是 NASA 月食图上 P./U. Radius 的口径,
// 换成地球赤道半径要乘以月球处的地球视差(弧度)。
// LunarEclipseShadowGeometry is the terrestrial-shadow geometry at maximum eclipse. Gamma is in Earth
// equatorial radii while the two shadow radii are in degrees, matching the P./U. Radius convention of
// NASA lunar-eclipse charts; multiply a radius by the Earth's parallax at the Moon to get Earth radii.
type LunarEclipseShadowGeometry struct {
// Gamma 是月心到地影轴的最小距离,单位地球赤道半径。
// Gamma is the least distance from the Moon's centre to the shadow axis, in Earth equatorial radii.
Gamma float64
// PenumbralRadiusDegrees 与 UmbralRadiusDegrees 是半影、本影在地影轴垂直面上的角半径,单位度。
// PenumbralRadiusDegrees and UmbralRadiusDegrees are the penumbral and umbral angular radii on the
// plane perpendicular to the shadow axis, in degrees.
PenumbralRadiusDegrees float64
UmbralRadiusDegrees float64
// MoonDistanceEarthRadii 是食甚时的地心月距,单位地球赤道半径。
// MoonDistanceEarthRadii is the geocentric lunar distance at greatest eclipse, in Earth equatorial radii.
MoonDistanceEarthRadii float64
// AxisDegrees 是食甚时月心到地影轴的角距,即 NASA 月食图上那列 Axis。
// 它与 Gamma 是同一个量的两种刻度:Gamma 除以月球处的地球视差就是它。
// AxisDegrees is the angular distance from the Moon's centre to the shadow axis at greatest eclipse,
// the column NASA lunar-eclipse charts print as Axis. It is the same quantity as Gamma on a different
// scale: divide Gamma by the Earth's parallax at the Moon.
AxisDegrees float64
}
// LunarEclipseShadowGeometryAt 用 Danjon 影半径模型计算食甚时刻的地影几何。
// LunarEclipseShadowGeometryAt computes the shadow geometry at maximum eclipse with the Danjon shadow model.
func LunarEclipseShadowGeometryAt(maximumJDE float64) LunarEclipseShadowGeometry {
return lunarEclipseShadowGeometryAt(maximumJDE, lunarEclipseShadowDanjon)
}
// LunarEclipseShadowGeometryChauvenetAt 用 Chauvenet 影半径模型计算食甚时刻的地影几何。
// LunarEclipseShadowGeometryChauvenetAt computes the shadow geometry with the Chauvenet shadow model.
func LunarEclipseShadowGeometryChauvenetAt(maximumJDE float64) LunarEclipseShadowGeometry {
return lunarEclipseShadowGeometryAt(maximumJDE, lunarEclipseShadowChauvenet)
}
// LunarEclipseShadowGeometryAtModel 按结果里记录的影半径模型取地影几何。
// LunarEclipseShadowGeometryAtModel picks the shadow geometry by the model recorded in a result.
func LunarEclipseShadowGeometryAtModel(maximumJDE float64, model LunarEclipseShadowModel) LunarEclipseShadowGeometry {
if model == LunarEclipseShadowModelChauvenet {
return LunarEclipseShadowGeometryChauvenetAt(maximumJDE)
}
return LunarEclipseShadowGeometryAt(maximumJDE)
}
func lunarEclipseShadowGeometryAt(maximumJDE float64, shadowModel lunarEclipseShadowModel) LunarEclipseShadowGeometry {
state := computeLunarShadowState(maximumJDE, shadowModel)
moonDistanceKM := HMoonAway(maximumJDE)
// 影半径是以地心为顶点的角量,除以月球处的地球视差就换成地球赤道半径。
earthParallax := lunarEarthEquatorialRadiusKM / moonDistanceKM
_, _, _, _, minimumDistance := refineLunarEclipseMaximum(maximumJDE, shadowModel)
if earthParallax <= 0 {
return LunarEclipseShadowGeometry{}
}
return LunarEclipseShadowGeometry{
Gamma: minimumDistance / earthParallax,
PenumbralRadiusDegrees: state.penumbraRadiusRad * 180 / math.Pi,
UmbralRadiusDegrees: state.umbraRadiusRad * 180 / math.Pi,
MoonDistanceEarthRadii: moonDistanceKM / lunarEarthEquatorialRadiusKM,
// 平面 x 向东(黄经差)、y 向北(黄纬和),方位角自北向东量。
AxisDegrees: minimumDistance * 180 / math.Pi,
}
}
+53
View File
@@ -0,0 +1,53 @@
package basic
import (
"math"
"testing"
"time"
)
// Axis 与 Gamma 是同一个量的两种刻度:Gamma × 月球处的地球视差 = Axis。
func TestLunarEclipseShadowGeometryAxisMatchesGamma(t *testing.T) {
for _, date := range []time.Time{
time.Date(2025, time.March, 14, 0, 0, 0, 0, time.UTC),
time.Date(2026, time.March, 3, 0, 0, 0, 0, time.UTC),
time.Date(2028, time.July, 6, 0, 0, 0, 0, time.UTC),
} {
result := LunarEclipse(UTC2TT(Date2JD(date)))
geometry := LunarEclipseShadowGeometryAt(result.Maximum)
// 影几何内部用的是小角近似的地球视差(R⊕/月距),这里必须用同一形式,否则二级差会露出来。
earthParallaxDegrees := (1 / geometry.MoonDistanceEarthRadii) * 180 / math.Pi
if delta := geometry.Gamma*earthParallaxDegrees - geometry.AxisDegrees; math.Abs(delta) > 1e-9 {
t.Fatalf("%s: Gamma×parallax - Axis = %g", date.Format("2006-01-02"), delta)
}
}
}
// 食分公式反解出的月心到影轴距离必须等于 Axis:这条把影几何与食分求解器对起来。
// 食分 = (影半径 + 月视半径 − Axis) / (2 × 月视半径)。
func TestLunarEclipseMagnitudeInvertsToAxis(t *testing.T) {
for _, date := range []time.Time{
time.Date(2025, time.March, 14, 0, 0, 0, 0, time.UTC),
time.Date(2026, time.March, 3, 0, 0, 0, 0, time.UTC),
time.Date(2028, time.July, 6, 0, 0, 0, 0, time.UTC),
time.Date(2020, time.November, 30, 0, 0, 0, 0, time.UTC),
} {
result := LunarEclipse(UTC2TT(Date2JD(date)))
geometry := LunarEclipseShadowGeometryAt(result.Maximum)
moonSemidiameter := MoonSemidiameter(result.Maximum) / 3600
fromUmbral := geometry.UmbralRadiusDegrees + moonSemidiameter -
2*moonSemidiameter*result.Magnitude
fromPenumbral := geometry.PenumbralRadiusDegrees + moonSemidiameter -
2*moonSemidiameter*result.PenumbralMagnitude
t.Logf("%s %s: Axis=%.4f 本影反推=%.4f 半影反推=%.4f",
date.Format("2006-01-02"), result.Type, geometry.AxisDegrees, fromUmbral, fromPenumbral)
if math.Abs(fromUmbral-geometry.AxisDegrees) > 0.002 {
t.Fatalf("%s: umbral magnitude implies axis %.4f, geometry gives %.4f",
date.Format("2006-01-02"), fromUmbral, geometry.AxisDegrees)
}
if math.Abs(fromPenumbral-geometry.AxisDegrees) > 0.002 {
t.Fatalf("%s: penumbral magnitude implies axis %.4f, geometry gives %.4f",
date.Format("2006-01-02"), fromPenumbral, geometry.AxisDegrees)
}
}
}
+329
View File
@@ -0,0 +1,329 @@
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: JDCalc(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: JDCalc(2023, 5, 5),
expectedType: LunarEclipsePenumbral,
expectedPenumbralStart: 2460070.1342392800,
expectedPenumbralEnd: 2460070.3159191823,
},
{
name: "2023-10-28 partial",
jde: JDCalc(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: JDCalc(2024, 3, 25),
expectedType: LunarEclipsePenumbral,
expectedPenumbralStart: 2460394.7028870000,
expectedPenumbralEnd: 2460394.8999071894,
},
{
name: "2024-09-18 partial",
jde: JDCalc(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: JDCalc(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: JDCalc(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: JDCalc(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)
}
assertContactJD(t, "PenumbralStart", result.PenumbralStart, tc.expectedPenumbralStart, timeTolerance)
assertContactJD(t, "PenumbralEnd", result.PenumbralEnd, tc.expectedPenumbralEnd, timeTolerance)
assertContactJD(t, "PartialStart", result.PartialStart, tc.expectedPartialStart, timeTolerance)
assertContactJD(t, "PartialEnd", result.PartialEnd, tc.expectedPartialEnd, timeTolerance)
assertContactJD(t, "TotalStart", result.TotalStart, tc.expectedTotalStart, timeTolerance)
assertContactJD(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 := JDCalc(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: JDCalc(2024, 3, 25), calc: LunarEclipse},
{name: "danjon 2024-03-25", jde: JDCalc(2024, 3, 25), calc: LunarEclipseDanjon},
{name: "chauvenet 2023-05-05", jde: JDCalc(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: JDCalc(2023, 10, 28),
expectedType: LunarEclipsePartial,
nasaPenumbralMagnitude: 1.1181,
nasaUmbralMagnitude: 0.1220,
},
{
name: "2025-03-14 total",
jde: JDCalc(2025, 3, 14),
expectedType: LunarEclipseTotal,
nasaPenumbralMagnitude: 2.2595,
nasaUmbralMagnitude: 1.1784,
},
{
name: "2026-03-03 total",
jde: JDCalc(2026, 3, 3),
expectedType: LunarEclipseTotal,
nasaPenumbralMagnitude: 2.1838,
nasaUmbralMagnitude: 1.1507,
},
{
name: "2026-08-28 partial",
jde: JDCalc(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(JDCalc(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)
}
for name, value := range map[string]float64{
"PenumbralStart": result.PenumbralStart,
"PenumbralEnd": result.PenumbralEnd,
"PartialStart": result.PartialStart,
"PartialEnd": result.PartialEnd,
"TotalStart": result.TotalStart,
"TotalEnd": result.TotalEnd,
} {
if !math.IsNaN(value) {
t.Fatalf("%s = %.12f, want NaN for a non-eclipse: %+v", name, value, 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)
}
}
// assertContactJD 未发生阶段的期望值是 0(基准表写法),此时实际值必须是 NaN 而不是 JD 0。
func assertContactJD(t *testing.T, name string, got, want, tolerance float64) {
t.Helper()
if want == 0 {
if !math.IsNaN(got) {
t.Fatalf("%s = %.12f, want NaN for an absent phase", name, got)
}
return
}
assertCloseJD(t, name, got, want, tolerance)
}
+217
View File
@@ -0,0 +1,217 @@
package basic
import "math"
func GetLunar(year, month, day int, tz float64) (lyear, lmonth, lday int, leap bool, result string) {
julianDayEpoch := JDCalc(year, month, float64(day))
// 确定农历年份
lyear = year
adjustedYear := year
if month == 11 || month == 12 {
winterSolsticeDay := GetJQTime(year, 270) + tz
firstNewMoonDay := TT2UTC(CalcMoonSHByJDE(winterSolsticeDay-16, 0)) + tz
nextNewMoonDay := TT2UTC(CalcMoonSHByJDE(firstNewMoonDay+28, 0)) + 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
}
+122 -370
View File
@@ -7,450 +7,202 @@ import (
. "b612.me/astro/tools"
)
func MarsL(JD float64) float64 {
return planet.WherePlanet(3, 0, JD)
func MarsL(jde float64) float64 {
return planet.WherePlanet(3, 0, jde)
}
func MarsB(JD float64) float64 {
return planet.WherePlanet(3, 1, JD)
func MarsB(jde float64) float64 {
return planet.WherePlanet(3, 1, jde)
}
func MarsR(JD float64) float64 {
return planet.WherePlanet(3, 2, JD)
func MarsR(jde float64) float64 {
return planet.WherePlanet(3, 2, jde)
}
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(jde float64) float64 {
l := MarsL(jde)
b := MarsB(jde)
r := MarsR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x
}
func AMarsY(JD float64) float64 {
func AMarsY(jde 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(jde)
b := MarsB(jde)
r := MarsR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
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(jde float64) float64 {
//l := MarsL(jde)
b := MarsB(jde)
r := MarsR(jde)
// el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
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(jde float64) (float64, float64, float64) {
l := MarsL(jde)
b := MarsB(jde)
r := MarsR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
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(jde float64) float64 {
lo, bo := MarsApparentLoBo(jde)
eps := TrueObliquity(jde)
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(jde float64) float64 {
lo, bo := MarsApparentLoBo(jde)
eps := TrueObliquity(jde)
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(jde float64) (float64, float64) {
lo, bo := MarsApparentLoBo(jde)
eps := TrueObliquity(jde)
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) + HJZD(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+=HJZD(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 += HJZD(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(jde float64) float64 {
sunDistance := MarsR(jde)
earthDistance := EarthMarsAway(jde)
earthSunDistance := planet.WherePlanet(-1, 2, jde)
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 {
func MarsHeight(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJde := jde - timezone/24.0
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := MarsApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := MarsApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*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)
}
func MarsAzimuth(jde, lon, lat, timezone float64) float64 {
func MarsAzimuth(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJde := jde - timezone/24.0
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := MarsApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := MarsApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*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(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - MarsApparentRa(UTC2TT(jd-timezone/24.0))
if hourAngle < 0 {
hourAngle += 360
}
return timeangle
return hourAngle
}
func MarsCulminationTime(jde, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
func MarsCulminationTime(localJD, lon, timezone float64) float64 {
// localJD 是本地民用日锚点(当地 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
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-MarsHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := MarsHourAngle(localJD, 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 {
break
}
var ok bool
estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 {
hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360
hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001
return hourAngleDelta / hourAngleSlope
})
if !ok {
return math.NaN()
}
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 UTC2TT(Date2JD(date.UTC()))
}
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)
}
}
}
}
+286
View File
@@ -0,0 +1,286 @@
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
marsStationDerivativeStepDay = 0.01
marsStationCoarseStepDay = 6.0
marsStationHalfWindowDay = 6.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
if !isFiniteFloat(jde) || !isFiniteFloat(degree) {
return math.NaN()
}
daysPerDegree := MARS_S_PERIOD / 360
currentDelta := marsSunLongitudeDelta(jde, degree, false)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
jde += daysPerDegree * currentDelta
}
estimateJDE := jde
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJDE+0.000005, degree, true) - marsSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
}
if !converged {
return math.NaN()
}
return TT2UTC(estimateJDE)
}
func marsConjunction(jde, degree float64, next uint8) float64 {
//0=last 1=next
if !isFiniteFloat(jde) || !isFiniteFloat(degree) {
return math.NaN()
}
daysPerDegree := MARS_S_PERIOD / 360
currentDelta := marsSunLongitudeDelta(jde, degree, false)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
jde += daysPerDegree * currentDelta
}
estimateJDE := jde
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDeltaN(prevJDE, degree, true, marsEventSearchN)
longitudeSlope := (marsSunLongitudeDeltaN(prevJDE+0.000005, degree, true, marsEventSearchN) - marsSunLongitudeDeltaN(prevJDE-0.000005, degree, true, marsEventSearchN)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= marsPhaseCoarseTolerance {
converged = true
break
}
}
if !converged {
return math.NaN()
}
converged = false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJDE+0.000005, degree, true) - marsSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
}
if !converged {
return math.NaN()
}
return TT2UTC(estimateJDE)
}
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 {
if !isFiniteFloat(oppositionJD) {
return math.NaN()
}
oppositionTT := UTC2TT(oppositionJD)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
easternQuadratureUT := marsConjunction(oppositionTT, 90, 0)
startTT = UTC2TT(easternQuadratureUT)
} else {
westernQuadratureUT := marsConjunction(oppositionTT, 270, 1)
endTT = UTC2TT(westernQuadratureUT)
}
bestJDE := zeroEventInWindow(startTT, endTT, marsStationCoarseStepDay, marsStationHalfWindowDay, 30.0/86400.0, func(jd float64) float64 {
return marsRADerivativeN(jd, marsStationDerivativeStepDay, marsEventSearchN)
}, func(jd float64) float64 {
return marsRADerivative(jd, marsStationDerivativeStepDay)
})
return TT2UTC(bestJDE)
}
func NextMarsRetrogradeToPrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
date = marsRetrogradeAroundOpposition(nextOppositionJD, false)
if !isFiniteFloat(date) || !stationUTQueryAfterOrEqual(date, jde) {
return math.NaN()
}
return date
}
func LastMarsRetrogradeToPrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
if !isFiniteFloat(lastOppositionJD) {
return math.NaN()
}
previousOppositionJD := marsConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
if !isFiniteFloat(previousOppositionJD) {
return math.NaN()
}
date = marsRetrogradeAroundOpposition(previousOppositionJD, false)
if !isFiniteFloat(date) || !stationUTQueryBeforeOrEqual(date, jde) {
return math.NaN()
}
return date
}
func NextMarsProgradeToRetrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
followingOppositionJD := marsConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
if !isFiniteFloat(followingOppositionJD) {
return math.NaN()
}
date = marsRetrogradeAroundOpposition(followingOppositionJD, true)
if !isFiniteFloat(date) || !stationUTQueryAfterOrEqual(date, jde) {
return math.NaN()
}
return date
}
func LastMarsProgradeToRetrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
if !isFiniteFloat(lastOppositionJD) {
return math.NaN()
}
date = marsRetrogradeAroundOpposition(lastOppositionJD, true)
if !isFiniteFloat(date) || !stationUTQueryBeforeOrEqual(date, jde) {
return math.NaN()
}
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)))
}
}
+56
View File
@@ -0,0 +1,56 @@
package basic
import (
"testing"
)
// 这些基准是"性能不回退"的度量口径:Nutation2000B 有界记忆表、升落上下文 stateAt 的分配、
// 以及两条真实调用链(月球升落、掩星升落回归)。改动这些热路径后请对比 benchmark 数字。
// These benchmarks are the regression yardstick for the hot paths changed here: the bounded
// Nutation2000B memo, the allocation behaviour of the rise/set context stateAt, and two real call
// chains (moon rise/set and the occultation rise/set regression case). Compare before/after numbers
// whenever those paths change.
func BenchmarkNutation2000B(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = Nutation2000B(2460310.5 + float64(i%97)*0.37)
}
}
func BenchmarkTrueObliquity(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = TrueObliquity(2460310.5 + float64(i%97)*0.37)
}
}
var benchMoonRise, benchMoonSet float64
func BenchmarkMoonRiseSetChain(b *testing.B) {
jd := JDCalc(2023, 6, 21)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
benchMoonRise, _ = GetMoonRiseTime(jd, 116.4074, 39.9042, 8, 1, 0)
benchMoonSet, _ = GetMoonSetTime(jd, 116.4074, 39.9042, 8, 1, 0)
}
}
// Nutation2000B 有界记忆表:重复瞬时必须命中且数值逐位不变。
func TestNutationMemoHitsRepeatedInstants(t *testing.T) {
resetNutationMemo()
const jd = 2460310.5
firstPsi, firstEps := Nutation2000B(jd)
secondPsi, secondEps := Nutation2000B(jd)
if firstPsi != secondPsi || firstEps != secondEps {
t.Fatalf("memo changed the value: (%v,%v) vs (%v,%v)", firstPsi, firstEps, secondPsi, secondEps)
}
hits, misses := nutationMemoStats()
if hits == 0 || misses == 0 {
t.Fatalf("expected one miss and at least one hit, got hits=%d misses=%d", hits, misses)
}
// 与未走记忆表的实现逐位一致。
directPsi, directEps := nutation2000BCompute(jd)
if firstPsi != directPsi || firstEps != directEps {
t.Fatalf("memoized value differs from direct computation")
}
}
+113 -530
View File
@@ -7,602 +7,185 @@ import (
. "b612.me/astro/tools"
)
func MercuryL(JD float64) float64 {
return planet.WherePlanet(1, 0, JD)
func MercuryL(jde float64) float64 {
return planet.WherePlanet(1, 0, jde)
}
func MercuryB(JD float64) float64 {
return planet.WherePlanet(1, 1, JD)
func MercuryB(jde float64) float64 {
return planet.WherePlanet(1, 1, jde)
}
func MercuryR(JD float64) float64 {
return planet.WherePlanet(1, 2, JD)
func MercuryR(jde float64) float64 {
return planet.WherePlanet(1, 2, jde)
}
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(jde float64) float64 {
l := MercuryL(jde)
b := MercuryB(jde)
r := MercuryR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x
}
func AMercuryY(JD float64) float64 {
func AMercuryY(jde 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(jde)
b := MercuryB(jde)
r := MercuryR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
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(jde float64) float64 {
//l := MercuryL(jde)
b := MercuryB(jde)
r := MercuryR(jde)
// el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
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(jde float64) (float64, float64, float64) {
l := MercuryL(jde)
b := MercuryB(jde)
r := MercuryR(jde)
el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jde)
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(jde float64) float64 {
lo, bo := MercuryApparentLoBo(jde)
return LoToRa(jde, 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(jde float64) float64 {
lo, bo := MercuryApparentLoBo(jde)
eps := TrueObliquity(jde)
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(jde float64) (float64, float64) {
lo, bo := MercuryApparentLoBo(jde)
return LoBoToRaDec(jde, 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 += HJZD(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+=HJZD(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) + HJZD(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(jde float64) float64 {
sunDistance := MercuryR(jde)
earthDistance := EarthMercuryAway(jde)
earthSunDistance := planet.WherePlanet(-1, 2, jde)
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 {
func MercuryHeight(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJde := jde - timezone/24.0
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := MercuryApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := MercuryApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*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)
}
func MercuryAzimuth(jde, lon, lat, timezone float64) float64 {
func MercuryAzimuth(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJde := jde - timezone/24.0
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := MercuryApparentRaDec(TD2UT(utcJde, true))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon)
ra, dec := MercuryApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*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(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - MercuryApparentRa(UTC2TT(jd-timezone/24.0))
if hourAngle < 0 {
hourAngle += 360
}
return timeangle
return hourAngle
}
func MercuryCulminationTime(jde, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时
func MercuryCulminationTime(localJD, lon, timezone float64) float64 {
// localJD 是本地民用日锚点(当地 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
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-MercuryHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := MercuryHourAngle(localJD, 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 {
break
}
var ok bool
estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 {
hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360
hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001
return hourAngleDelta / hourAngleSlope
})
if !ok {
return math.NaN()
}
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 UTC2TT(Date2JD(date.UTC()))
}
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)
}
}
}
}
+810
View File
@@ -0,0 +1,810 @@
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
mercuryStationWindowDays = 30.0
mercuryStationDerivativeStepDay = 0.01
mercuryStationCoarseStepDay = 2.0
mercuryStationHalfWindowDay = 2.0
mercuryStationMotionTolerance = 1e-3
// mercuryStationAnchorAttempts 类型化「留」在锚点不满足侧向不变量时,最多推进/回退几个会合周期。
mercuryStationAnchorAttempts = 4
// mercuryConjunctionSameInstantDegrees 合搜索「同刻」快速路径的视黄经差触发阈值(度)。
// 0.05 度约合 30 分钟的时间跨度,远大于截断级数在根附近的误差,
// 从而保证「查询落在合附近」时一定走精确解 + 侧向判定,而不是被方向扫描跨过去。
mercuryConjunctionSameInstantDegrees = 5.0e-2
// mercuryConjunctionScanStepDay / mercuryConjunctionScanMaxSteps 方向性括号扫描参数:
// 8 天一步 × 80 步 = 640 天,覆盖一个水星会合周期(115.88 天)且有大量余量。
mercuryConjunctionScanStepDay = 8.0
mercuryConjunctionScanMaxSteps = 80
// mercuryConjunctionPolishToleranceDay 全项抛光的时间容差(0.01 秒):
// 割线法从 8 天括号收敛到该量级只需多 1~2 次求值,但把合时刻精度保持在毫秒级
// (与旧实现牛顿法的 1e-5 天口径一致)。
mercuryConjunctionPolishToleranceDay = 0.01 / 86400.0
)
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, jde float64, n int) mercuryConjunctionLBR {
return mercuryConjunctionLBR{
lo: planet.WherePlanetN(planetIndex, 0, jde, n),
bo: planet.WherePlanetN(planetIndex, 1, jde, n),
r: planet.WherePlanetN(planetIndex, 2, jde, 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(jde, targetDeg float64, n int) float64 {
planetLo := planet.WherePlanetN(1, 0, jde, n)
earthLo := planet.WherePlanetN(-1, 0, jde, 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(jde float64) float64 {
return mercuryConjunctionAngleDelta(MercuryApparentLo(jde) - HSunApparentLo(jde))
}
func mercuryConjunctionApproxTT(seed float64, inferior bool) float64 {
heliocentricTarget := 180.0
if inferior {
heliocentricTarget = 0
}
jde := seed
for i := 0; i < 6; i++ {
jde -= mercuryConjunctionHeliocentricDelta(jde, heliocentricTarget, 8) / (360.0 / MERCURY_S_PERIOD)
}
startSample := mercuryConjunctionDifference(jde, 8, 0, 0, 0)
nextSample := mercuryConjunctionDifference(jde+mercuryConjunctionDerivativeStepDay, 8, 0, 0, 0)
diffSlope := mercuryConjunctionAngleDelta(nextSample.diff-startSample.diff) / mercuryConjunctionDerivativeStepDay
refined := mercuryConjunctionDifference(jde, 40, 0, startSample.sunLightDays, startSample.geoLightDays)
jde -= refined.diff / diffSlope
final := mercuryConjunctionDifference(jde, -1, 0, refined.sunLightDays, refined.geoLightDays)
jde -= final.diff / diffSlope
return jde
}
func mercuryConjunctionExactTT(seed float64, inferior bool) float64 {
estimateJD := mercuryConjunctionApproxTT(seed, inferior)
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJD
longitudeDelta := mercuryConjunctionExactDelta(prevJDE)
longitudeSlope := (mercuryConjunctionExactDelta(prevJDE+0.000005) - mercuryConjunctionExactDelta(prevJDE-0.000005)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true
break
}
}
if !converged {
return math.NaN()
}
return estimateJD
}
// mercuryConjunction 在 jde 的指定方向上求最近一次水星合(内合/外合)。
//
// 旧实现用「启发式跳 + 2 天一步走」:跳过头落到合之后时,|Δ| 会持续变大,
// 于是走满一个会合周期、跳过一次合(例如 2008-01-22 查到 2008-06-07 而不是 2008-02-06),
// 并且下游的类型化「留」会因此返回错事件、甚至让 Last 返回未来。
// 新实现改成方向性括号扫描:截断级数逐段找异号区间(顺序扫描 ⇒ 一定取最近的一个合),
// 再用全项级数在括号内抛光;扫满 640 天仍无括号时有界返回 NaN。
func mercuryConjunction(jde float64, next uint8) float64 {
//0=last 1=next
if !isFiniteFloat(jde) {
return math.NaN()
}
if math.Abs(mercuryConjunctionExactDelta(jde)) <= mercuryConjunctionSameInstantDegrees {
best := math.NaN()
consider := func(inferior bool) {
eventUT := TT2UTC(mercuryConjunctionExactTT(jde, inferior))
if !isFiniteFloat(eventUT) {
return
}
if next == 0 && !eventUTQueryBeforeOrEqual(eventUT, jde) {
return
}
if next == 1 && !eventUTQueryAfterOrEqual(eventUT, jde) {
return
}
if math.IsNaN(best) || math.Abs(eventUTQueryTTDelta(eventUT, jde)) < math.Abs(eventUTQueryTTDelta(best, jde)) {
best = eventUT
}
}
consider(true)
consider(false)
if !math.IsNaN(best) {
return best
}
}
direction := 1.0
if next == 0 {
direction = -1
}
leftJDE := jde
leftValue := mercuryConjunctionDeltaN(leftJDE, mercuryEventSearchN)
if !isFiniteFloat(leftValue) {
return math.NaN()
}
for i := 0; i < mercuryConjunctionScanMaxSteps; i++ {
rightJDE := jde + direction*mercuryConjunctionScanStepDay*float64(i+1)
rightValue := mercuryConjunctionDeltaN(rightJDE, mercuryEventSearchN)
if !isFiniteFloat(rightValue) {
return math.NaN()
}
if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 {
return mercuryConjunctionPolish(jde, leftJDE, rightJDE, direction)
}
leftJDE, leftValue = rightJDE, rightValue
}
return math.NaN()
}
// mercuryConjunctionDeltaN 截断级数下的水星-太阳视黄经差(度,[-180,180]),用于方向性括号扫描。
func mercuryConjunctionDeltaN(jde float64, n int) float64 {
return mercuryConjunctionAngleDelta(MercuryApparentLoN(jde, n) - HSunApparentLoN(jde, n))
}
// mercuryConjunctionPolish 用全项级数在截断级数给出的括号内抛光。
// 截断误差可能让括号两端在全项函数上同号(罕见),此时沿扫描方向再扩一两个扫描步;
// 若仍未被确认(典型情形:查询几乎正好落在合上,截断级数在根两侧的符号与全项不一致),
// 退回全项级数的方向扫描,保证有界且不返回 NaN。
func mercuryConjunctionPolish(jde, leftJDE, rightJDE, direction float64) float64 {
for attempt := 0; attempt < 3; attempt++ {
leftValue := mercuryConjunctionExactDelta(leftJDE)
rightValue := mercuryConjunctionExactDelta(rightJDE)
if !isFiniteFloat(leftValue) || !isFiniteFloat(rightValue) {
return math.NaN()
}
if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 {
root, ok := eventBracketSecantRoot(leftJDE, rightJDE, leftValue, rightValue,
mercuryConjunctionPolishToleranceDay, mercuryConjunctionExactDelta)
if !ok {
return math.NaN()
}
return TT2UTC(root)
}
if direction > 0 {
rightJDE += mercuryConjunctionScanStepDay
continue
}
leftJDE -= mercuryConjunctionScanStepDay
}
return mercuryConjunctionFullDirectionalScan(jde, direction)
}
// mercuryConjunctionFullDirectionalScan 全项级数的方向扫描(截断括号未被确认时的兜底)。
func mercuryConjunctionFullDirectionalScan(jde, direction float64) float64 {
leftJDE := jde
leftValue := mercuryConjunctionExactDelta(leftJDE)
if !isFiniteFloat(leftValue) {
return math.NaN()
}
for i := 0; i < mercuryConjunctionScanMaxSteps; i++ {
rightJDE := jde + direction*mercuryConjunctionScanStepDay*float64(i+1)
rightValue := mercuryConjunctionExactDelta(rightJDE)
if !isFiniteFloat(rightValue) {
return math.NaN()
}
if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 {
root, ok := eventBracketSecantRoot(leftJDE, rightJDE, leftValue, rightValue,
mercuryConjunctionPolishToleranceDay, mercuryConjunctionExactDelta)
if !ok {
return math.NaN()
}
return TT2UTC(root)
}
leftJDE, leftValue = rightJDE, rightValue
}
return math.NaN()
}
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 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 mercuryRADerivativeN(jde, delta float64, n int) float64 {
sub := MercuryApparentRaN(jde+delta, n) - MercuryApparentRaN(jde-delta, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func mercuryStationInWindow(startTT, endTT float64) float64 {
bestJDE := zeroEventInWindow(startTT, endTT, mercuryStationCoarseStepDay, mercuryStationHalfWindowDay, 30.0/86400.0, func(jd float64) float64 {
return mercuryRADerivativeN(jd, mercuryStationDerivativeStepDay, mercuryEventSearchN)
}, func(jd float64) float64 {
return mercuryRADerivative(jd, mercuryStationDerivativeStepDay)
})
return TT2UTC(bestJDE)
}
func mercuryStationBetween(startTT, endTT float64) bool {
if endTT < startTT {
startTT, endTT = endTT, startTT
}
if endTT-startTT <= 0 {
return false
}
// 跨度超过一个回留窗口时截断扫描已不可靠:返回 false 不再阻断快速路径。
// 此时候选事件由 mercuryConjunction 的完整方向扫描保证,快速路径无需再复核。
// A span beyond the station window makes the truncated scan unreliable; report false so the
// fast path is not blocked. The typed candidate is already guaranteed by mercuryConjunction's
// exhaustive directional scan.
if endTT-startTT > mercuryStationWindowDays {
return false
}
// 截断扫描足以判断单候选快速路径是否安全 / A truncated scan is enough to decide whether the one-candidate fast path is safe.
left := startTT
leftValue := mercuryRADerivativeN(left, mercuryStationDerivativeStepDay, mercuryEventSearchN)
for left < endTT {
right := left + mercuryStationCoarseStepDay
if right > endTT {
right = endTT
}
rightValue := mercuryRADerivativeN(right, mercuryStationDerivativeStepDay, mercuryEventSearchN)
if leftValue == 0 || leftValue*rightValue < 0 || rightValue == 0 {
return true
}
left = right
leftValue = rightValue
}
return false
}
func mercuryProgradeToRetrogradeAroundInferior(inferiorUT float64) float64 {
inferiorTT := UTC2TT(inferiorUT)
return mercuryStationInWindow(inferiorTT-mercuryStationWindowDays, inferiorTT)
}
func mercuryRetrogradeToProgradeAroundInferior(inferiorUT float64) float64 {
inferiorTT := UTC2TT(inferiorUT)
return mercuryStationInWindow(inferiorTT, inferiorTT+mercuryStationWindowDays)
}
// 类型化「留」统一结构:以下合为锚点求候选,再用侧向不变量(Next 不得早于查询、
// Last 不得晚于查询)校验;不满足则把锚点推进/回退一个会合周期重试,最多
// mercuryStationAnchorAttempts 次,最终仍有界失败为 NaN。
// 这保证 Last* 永远不会返回未来、Next* 永远不会返回过去的事件。
func NextMercuryProgradeToRetrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
inferior := NextMercuryInferiorConjunction(jde)
for i := 0; i < mercuryStationAnchorAttempts && isFiniteFloat(inferior); i++ {
date := mercuryProgradeToRetrogradeAroundInferior(inferior)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
inferior = NextMercuryInferiorConjunction(eventUTNextQueryTT(inferior))
}
return math.NaN()
}
func NextMercuryRetrogradeToPrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
inferior := LastMercuryInferiorConjunction(jde)
for i := 0; i < mercuryStationAnchorAttempts && isFiniteFloat(inferior); i++ {
date := mercuryRetrogradeToProgradeAroundInferior(inferior)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
inferior = NextMercuryInferiorConjunction(eventUTNextQueryTT(inferior))
}
return math.NaN()
}
func LastMercuryProgradeToRetrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
inferior := NextMercuryInferiorConjunction(jde)
for i := 0; i < mercuryStationAnchorAttempts && isFiniteFloat(inferior); i++ {
date := mercuryProgradeToRetrogradeAroundInferior(inferior)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
inferior = LastMercuryInferiorConjunction(eventUTLastQueryTT(inferior))
}
return math.NaN()
}
func LastMercuryRetrogradeToPrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
inferior := LastMercuryInferiorConjunction(jde)
for i := 0; i < mercuryStationAnchorAttempts && isFiniteFloat(inferior); i++ {
date := mercuryRetrogradeToProgradeAroundInferior(inferior)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
inferior = LastMercuryInferiorConjunction(eventUTLastQueryTT(inferior))
}
return math.NaN()
}
// earliestFiniteEventUT / latestFiniteEventUT 在候选中取最早/最晚的有限值(全部非有限时返回 NaN)。
func earliestFiniteEventUT(candidates ...float64) float64 {
best := math.NaN()
for _, candidate := range candidates {
if !isFiniteFloat(candidate) {
continue
}
if math.IsNaN(best) || candidate < best {
best = candidate
}
}
return best
}
func latestFiniteEventUT(candidates ...float64) float64 {
best := math.NaN()
for _, candidate := range candidates {
if !isFiniteFloat(candidate) {
continue
}
if math.IsNaN(best) || candidate > best {
best = candidate
}
}
return best
}
func nextMercuryRetrogradeFromTyped(jde float64) float64 {
return earliestFiniteEventUT(NextMercuryProgradeToRetrograde(jde), NextMercuryRetrogradeToPrograde(jde))
}
func NextMercuryRetrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay)
if motion > mercuryStationMotionTolerance {
p2r := NextMercuryProgradeToRetrograde(jde)
if isFiniteFloat(p2r) && !mercuryStationBetween(jde, UTC2TT(p2r)) {
return p2r
}
best := earliestFiniteEventUT(p2r, NextMercuryRetrogradeToPrograde(jde))
if !stationUTQueryAfterOrEqual(best, jde) {
return math.NaN()
}
return best
}
if motion < -mercuryStationMotionTolerance {
r2p := NextMercuryRetrogradeToPrograde(jde)
if isFiniteFloat(r2p) && !mercuryStationBetween(jde, UTC2TT(r2p)) {
return r2p
}
best := earliestFiniteEventUT(NextMercuryProgradeToRetrograde(jde), r2p)
if !stationUTQueryAfterOrEqual(best, jde) {
return math.NaN()
}
return best
}
return nextMercuryRetrogradeFromTyped(jde)
}
func lastMercuryRetrogradeFromTyped(jde float64) float64 {
return latestFiniteEventUT(LastMercuryProgradeToRetrograde(jde), LastMercuryRetrogradeToPrograde(jde))
}
func LastMercuryRetrograde(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay)
if motion > mercuryStationMotionTolerance {
r2p := LastMercuryRetrogradeToPrograde(jde)
if isFiniteFloat(r2p) && !mercuryStationBetween(UTC2TT(r2p), jde) {
return r2p
}
best := latestFiniteEventUT(LastMercuryProgradeToRetrograde(jde), r2p)
if !stationUTQueryBeforeOrEqual(best, jde) {
return math.NaN()
}
return best
}
if motion < -mercuryStationMotionTolerance {
p2r := LastMercuryProgradeToRetrograde(jde)
if isFiniteFloat(p2r) && !mercuryStationBetween(UTC2TT(p2r), jde) {
return p2r
}
best := latestFiniteEventUT(p2r, LastMercuryRetrogradeToPrograde(jde))
if !stationUTQueryBeforeOrEqual(best, jde) {
return math.NaN()
}
return best
}
return lastMercuryRetrogradeFromTyped(jde)
}
func LastMercuryRetrogradeStrict(jde float64) float64 {
return LastMercuryRetrograde(jde)
}
func NextMercuryRetrogradeStrict(jde float64) float64 {
return NextMercuryRetrograde(jde)
}
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)
}
// mercuryGreatestElongationInWindow 求窗口内大距:目标是公开 MercurySunElongation 的极大(视距角),
// 而不是忽略光行差/视位置修正的真距角,否则返回的时刻不是调用方能量到的那个极值。
// 窗口两端是世界时,目标函数收力学时,因此逐次换算。
func mercuryGreatestElongationInWindow(start, end float64) float64 {
return maximizeInWindow(start, end, 2.0, func(utJD float64) float64 {
return mercurySunElongationN(UTC2TT(utJD), mercuryEventSearchN)
}, func(utJD float64) float64 {
return MercurySunElongation(UTC2TT(utJD))
})
}
func mercuryEastElongationWindowEndingAt(inferior float64) (float64, float64) {
lastSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(inferior))
return lastSuperior + innerEventWindowPadding, inferior - innerEventWindowPadding
}
func mercuryWestElongationWindowEndingAt(superior float64) (float64, float64) {
lastInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(superior))
return lastInferior + innerEventWindowPadding, superior - innerEventWindowPadding
}
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 !isFiniteFloat(jde) {
return math.NaN()
}
if east {
start, windowEnd := mercuryEastElongationWindowContaining(jde)
for i := 0; i < eventDirectionalSearchIterations; i++ {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if !isFiniteFloat(date) {
return math.NaN()
}
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextInferior := NextMercuryInferiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = mercuryEastElongationWindowEndingAt(nextInferior)
}
return math.NaN()
}
start, windowEnd := mercuryWestElongationWindowContaining(jde)
for i := 0; i < eventDirectionalSearchIterations; i++ {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if !isFiniteFloat(date) {
return math.NaN()
}
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextSuperior := NextMercurySuperiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = mercuryWestElongationWindowEndingAt(nextSuperior)
}
return math.NaN()
}
func lastMercuryGreatestElongationTyped(jde float64, east bool) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
if east {
start, windowEnd := mercuryEastElongationWindowContaining(jde)
for i := 0; i < eventDirectionalSearchIterations; i++ {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if !isFiniteFloat(date) {
return math.NaN()
}
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = mercuryEastElongationWindowEndingAt(prevInferior)
}
return math.NaN()
}
start, windowEnd := mercuryWestElongationWindowContaining(jde)
for i := 0; i < eventDirectionalSearchIterations; i++ {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if !isFiniteFloat(date) {
return math.NaN()
}
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = mercuryWestElongationWindowEndingAt(prevSuperior)
}
return math.NaN()
}
// mercuryElongationWindowAt 返回包含 jde 的该侧大距窗口;查询落在合的 4 秒内边距里时两侧都不包含。
func mercuryElongationWindowAt(jde float64, east bool) (float64, float64, bool) {
if east {
start, end := mercuryEastElongationWindowEndingAt(NextMercuryInferiorConjunction(jde))
return start, end, eventUTQueryBeforeOrEqual(start, jde) && eventUTQueryAfterOrEqual(end, jde)
}
start, end := mercuryWestElongationWindowEndingAt(NextMercurySuperiorConjunction(jde))
return start, end, eventUTQueryBeforeOrEqual(start, jde) && eventUTQueryAfterOrEqual(end, jde)
}
// 无东西侧参数的 Next/Last 先只看查询所在窗口那一侧:该侧大距未过就是答案,已过则另一侧紧接着的
// 下一个才是答案,因此只有在查询贴住合(两侧窗口都不含)时才退化为两侧都算。赤经差在合附近会提前
// 变号,不能用它定窗口。
func NextMercuryGreatestElongation(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
for _, east := range [2]bool{true, false} {
start, end, ok := mercuryElongationWindowAt(jde, east)
if !ok {
continue
}
if date := mercuryGreatestElongationInWindow(start, end); isFiniteFloat(date) && eventUTQueryAfterOrEqual(date, jde) {
return date
}
if date := nextMercuryGreatestElongationTyped(jde, !east); isFiniteFloat(date) && eventUTQueryAfterOrEqual(date, jde) {
return date
}
break
}
return earliestFiniteEventUT(nextMercuryGreatestElongationTyped(jde, true), nextMercuryGreatestElongationTyped(jde, false))
}
func LastMercuryGreatestElongation(jde float64) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
for _, east := range [2]bool{true, false} {
start, end, ok := mercuryElongationWindowAt(jde, east)
if !ok {
continue
}
if date := mercuryGreatestElongationInWindow(start, end); isFiniteFloat(date) && eventUTQueryBeforeOrEqual(date, jde) {
return date
}
if date := lastMercuryGreatestElongationTyped(jde, !east); isFiniteFloat(date) && eventUTQueryBeforeOrEqual(date, jde) {
return date
}
break
}
return latestFiniteEventUT(lastMercuryGreatestElongationTyped(jde, true), lastMercuryGreatestElongationTyped(jde, false))
}
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)
}
@@ -0,0 +1,31 @@
package basic
import (
"math"
"testing"
)
func TestMercuryRetrogradeFastPathKeepsTypedCandidateOrder(t *testing.T) {
// 这些查询覆盖一个普通古代周期和两个长间隔边界 / These queries cover a normal ancient cycle and two long-gap boundaries
//,类型化驻留搜索必须作为最终权威 / where the typed station search must remain the final authority.
for _, queryUT := range []float64{
1026548.810500779,
1644733.927538287,
1645082.416782375,
} {
queryTT := UTC2TT(queryUT)
nextP2R := NextMercuryProgradeToRetrograde(queryTT)
nextR2P := NextMercuryRetrogradeToPrograde(queryTT)
wantNext := math.Min(nextP2R, nextR2P)
if got := NextMercuryRetrograde(queryTT); math.Abs(got-wantNext) > 1e-7 {
t.Fatalf("next aggregate mismatch at %.9f: got %.12f want %.12f", queryUT, got, wantNext)
}
lastP2R := LastMercuryProgradeToRetrograde(queryTT)
lastR2P := LastMercuryRetrogradeToPrograde(queryTT)
wantLast := math.Max(lastP2R, lastR2P)
if got := LastMercuryRetrograde(queryTT); math.Abs(got-wantLast) > 1e-7 {
t.Fatalf("last aggregate mismatch at %.9f: got %.12f want %.12f", queryUT, got, wantLast)
}
}
}
+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 UTC2TT(Date2JD(time.Date(year, month, day, hour, minute, second, 0, loc).UTC()))
}
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", JD2DateByZone(nextP2R, loc, false), JD2DateByZone(wantP2R, loc, false))
}
if math.Abs(nextR2P-wantR2P) > tolerance {
t.Fatalf("next R2P mismatch: got %s want %s", JD2DateByZone(nextR2P, loc, false), JD2DateByZone(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", JD2DateByZone(lastP2R, loc, false), JD2DateByZone(wantP2R, loc, false))
}
if math.Abs(lastR2P-wantR2P) > tolerance {
t.Fatalf("last R2P mismatch: got %s want %s", JD2DateByZone(lastR2P, loc, false), JD2DateByZone(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("")
}
}
+48 -1694
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(jde float64) float64 {
return MoonBrightLimbPositionAngleN(jde, -1)
}
// MoonBrightLimbPositionAngleN 月亮明亮边缘位置角(截断版) / truncated position angle of the Moon's bright limb.
func MoonBrightLimbPositionAngleN(jde float64, n int) float64 {
sunRA, sunDec := HSunApparentRaDecN(jde, n)
moonRA, moonDec := HMoonTrueRaDecN(jde, n)
return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec)
}
// MoonTopocentricBrightLimbPositionAngle 月亮站心明亮边缘位置角 / topocentric position angle of the Moon's bright limb.
func MoonTopocentricBrightLimbPositionAngle(jde, observerLon, observerLat, height float64) float64 {
return MoonTopocentricBrightLimbPositionAngleN(jde, observerLon, observerLat, height, -1)
}
// MoonTopocentricBrightLimbPositionAngleN 月亮站心明亮边缘位置角(截断版) / truncated topocentric position angle of the Moon's bright limb.
func MoonTopocentricBrightLimbPositionAngleN(jde, observerLon, observerLat, height float64, n int) float64 {
sunRA, sunDec := sunTopocentricApparentRaDecN(jde, observerLon, observerLat, height, n)
moonRA, moonDec := moonTopocentricApparentRaDecN(jde, observerLon, observerLat, height, n)
return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec)
}
func moonTopocentricApparentRaDecN(jde, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA := HMoonTrueRaN(jde, n)
geocentricDec := HMoonTrueDecN(jde, n)
distanceAU := HMoonAwayN(jde, n) / moonPhysicalAstronomicalUnitKM
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TT2UTC(jde), distanceAU, height)
}
func sunTopocentricApparentRaDecN(jde, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA, geocentricDec := HSunApparentRaDecN(jde, n)
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TT2UTC(jde), EarthAwayN(jde, 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 := UTC2TT(Date2JD(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)))
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 := UTC2TT(Date2JD(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)))
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")
}
}
@@ -0,0 +1,64 @@
package basic
import (
"encoding/json"
"os"
"testing"
"time"
)
type moonGeocentricApparentSample struct {
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 TestMoonGeocentricApparentCoordinatesMatchHorizonsBaseline(t *testing.T) {
data, err := os.ReadFile("testdata/moon_geocentric_apparent_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []moonGeocentricApparentSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
if len(samples) == 0 {
t.Fatal("empty moon apparent baseline")
}
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 := UTC2TT(Date2JD(date.UTC()))
prefix := "moon." + sample.InputUTC
assertPlanetApparentAngleClose(t, prefix+".RightAscension", HMoonGeocentricApparentRa(jd), sample.RightAscension, 0.001)
assertPlanetPhaseClose(t, prefix+".Declination", HMoonGeocentricApparentDec(jd), sample.Declination, 0.001)
assertPlanetApparentAngleClose(t, prefix+".EclipticLongitude", HMoonApparentLo(jd), sample.EclipticLongitude, 0.001)
assertPlanetPhaseClose(t, prefix+".EclipticLatitude", HMoonTrueBo(jd), sample.EclipticLatitude, 0.001)
}
}
func TestMoonGeocentricTrueCoordinatesFollowDefinition(t *testing.T) {
samples := []time.Time{
time.Date(1900, 1, 14, 12, 0, 0, 0, time.UTC),
time.Date(1950, 6, 3, 0, 0, 0, 0, time.UTC),
time.Date(2000, 2, 29, 18, 0, 0, 0, time.UTC),
time.Date(2026, 1, 1, 6, 0, 0, 0, time.UTC),
time.Date(2100, 8, 17, 9, 0, 0, 0, time.UTC),
}
for _, sample := range samples {
jd := UTC2TT(Date2JD(sample.UTC()))
wantRA, wantDec := LoBoToRaDec(jd, HMoonTrueLo(jd), HMoonTrueBo(jd))
gotRA, gotDec := HMoonGeocentricTrueRaDec(jd)
assertPlanetApparentAngleClose(t, sample.Format(time.RFC3339)+".TrueRightAscension", gotRA, wantRA, 1e-12)
assertPlanetPhaseClose(t, sample.Format(time.RFC3339)+".TrueDeclination", gotDec, wantDec, 1e-12)
}
}
+30
View File
@@ -0,0 +1,30 @@
package basic
import (
"math"
"testing"
)
func TestHMoonGeocentricApparentRaDecComponentsMatch(t *testing.T) {
jd := UTC2TT(JDCalc(2026, 1, 1.25))
ra, dec := HMoonGeocentricApparentRaDec(jd)
if diff := math.Abs(ra - HMoonGeocentricApparentRa(jd)); diff > 1e-12 {
t.Fatalf("RA pair mismatch: got %.15f want %.15f", ra, HMoonGeocentricApparentRa(jd))
}
if diff := math.Abs(dec - HMoonGeocentricApparentDec(jd)); diff > 1e-12 {
t.Fatalf("Dec pair mismatch: got %.15f want %.15f", dec, HMoonGeocentricApparentDec(jd))
}
}
func TestHMoonGeocentricTrueRaDecComponentsMatch(t *testing.T) {
jd := UTC2TT(JDCalc(2026, 1, 1.25))
ra, dec := HMoonGeocentricTrueRaDec(jd)
if diff := math.Abs(ra - HMoonGeocentricTrueRa(jd)); diff > 1e-12 {
t.Fatalf("RA pair mismatch: got %.15f want %.15f", ra, HMoonGeocentricTrueRa(jd))
}
if diff := math.Abs(dec - HMoonGeocentricTrueDec(jd)); diff > 1e-12 {
t.Fatalf("Dec pair mismatch: got %.15f want %.15f", dec, HMoonGeocentricTrueDec(jd))
}
}
+15
View File
@@ -0,0 +1,15 @@
package basic
// MoonHorizon 返回 UTC 儒略日下海平面几何月球中心地平圈(月球恰好在地平线上的观测者轨迹,
// 即月下点周围的地平圈)的 [经度, 纬度] 顶点,单位为度,不重复首点。视差与椭球口径同
// HMoonHeight,不含折射;与 HMoonHeight(经, 纬, ..., 0) 配合时该圈上的点高度角为 0。
// samples<=0 取 360,其余夹到 [12, 1440]。
// MoonHorizon returns sea-level geometric Moon-centre horizon vertices in degrees for a UTC Julian
// day: the locus of observers that see the Moon exactly on the horizon. Parallax and the observer
// ellipsoid match HMoonHeight; refraction is excluded.
func MoonHorizon(jdUTC float64, samples int) [][2]float64 {
if !finite(jdUTC) {
return nil
}
return MoonStateAt(jdUTC).MoonHorizon(samples)
}
+114
View File
@@ -0,0 +1,114 @@
package basic
import (
"math"
"testing"
"time"
)
func TestMoonHorizonMatchesTopocentricAltitude(t *testing.T) {
for _, jd := range []float64{JDCalc(2026, 3, 3), JDCalc(2025, 9, 7), JDCalc(2024, 12, 15)} {
points := MoonHorizon(jd, 360)
if len(points) != 360 {
t.Fatalf("horizon points=%d", len(points))
}
for _, point := range points {
if altitude := HMoonHeight(jd, point[0], point[1], 0); math.Abs(altitude) > 1e-9 {
t.Fatalf("JD=%v point=%v altitude=%g, want zero", jd, point, altitude)
}
}
}
if MoonHorizon(math.NaN(), 360) != nil {
t.Fatal("invalid JD accepted")
}
}
// TestMoonHorizonUsesUTCInput 固定地平圈的时标口径:入参是 UTC 儒略日,站心恒星时按 UTC→UT1 换算。
// 取 DUT1 明显的两个时刻,确认圈上点只在 UTC 口径下高度角为零,且两种口径在本地可区分。
func TestMoonHorizonUsesUTCInput(t *testing.T) {
for _, at := range []time.Time{
time.Date(1980, 3, 15, 18, 0, 0, 0, time.UTC),
time.Date(2035, 3, 15, 18, 0, 0, 0, time.UTC),
} {
jdUTC := Date2JD(at)
dut1Seconds := (UTC2UT1(jdUTC) - jdUTC) * 86400
if math.Abs(dut1Seconds) < 0.3 {
t.Fatalf("%s DUT1=%.3f s 太小,区分不出两种口径", at.Format("2006-01-02"), dut1Seconds)
}
jdAsUT1 := jdUTC + dut1Seconds/86400
distinguishable := false
for _, point := range MoonHorizon(jdUTC, 360) {
if altitude := HMoonHeight(jdUTC, point[0], point[1], 0); math.Abs(altitude) > 1e-9 {
t.Fatalf("%s UTC 口径下圈上点高度角=%g,应为零", at.Format("2006-01-02"), altitude)
}
if altitude := HMoonHeight(jdAsUT1, point[0], point[1], 0); math.Abs(altitude) > 1e-4 {
distinguishable = true
}
}
if !distinguishable {
t.Fatalf("%s 两种口径不可区分,用例失去意义", at.Format("2006-01-02"))
}
if MoonHorizon(jdAsUT1, 360)[0] == MoonHorizon(jdUTC, 360)[0] {
t.Fatalf("%s UTC 与 UT1 两种读法给出了同一条圈", at.Format("2006-01-02"))
}
}
}
// TestMoonStateHorizonMatchesPackageHorizon 固定 MoonState 派生量与包级函数逐位一致,以及采样数与非法入参的兜底。
func TestMoonStateHorizonMatchesPackageHorizon(t *testing.T) {
jd := JDCalc(2026, 3, 3)
state := MoonStateAt(jd)
for _, samples := range []int{0, 1, 12, 360, 5000} {
got := state.MoonHorizon(samples)
want := MoonHorizon(jd, samples)
if len(got) != len(want) {
t.Fatalf("samples=%d 点数 %d,包级 %d", samples, len(got), len(want))
}
for index := range got {
if got[index] != want[index] {
t.Fatalf("samples=%d 第 %d 点 %v,包级 %v", samples, index, got[index], want[index])
}
}
}
for _, invalid := range []float64{math.NaN(), math.Inf(1), math.Inf(-1)} {
if points := MoonStateAt(invalid).MoonHorizon(360); points != nil {
t.Fatalf("非有限 UTC 儒略日 %v 仍给出地平圈", invalid)
}
if points := MoonHorizon(invalid, 360); points != nil {
t.Fatalf("非有限 UTC 儒略日 %v 仍给出地平圈", invalid)
}
}
if altitude := MoonStateAt(math.NaN()).HMoonHeight(0, 0); !math.IsNaN(altitude) {
t.Fatalf("非有限状态的高度角=%v,期望 NaN", altitude)
}
}
// TestHMoonHeightUsesLocalCivilFrame 固定 HMoonHeight 的时标框架:jd 是当地民用时(墙上时刻),
// tz 是时区偏移小时数。同一物理时刻写成「当地民用时 + tz」与「UTC 数值 + tz=0」必须一致;
// 把 UTC 数值再配非零 tz 会多减一次时区,必须能区分。
func TestHMoonHeightUsesLocalCivilFrame(t *testing.T) {
utc := time.Date(2029, 1, 1, 16, 0, 0, 0, time.UTC)
zone := time.FixedZone("CST", 8*3600)
utcJD := Date2JD(utc)
localJD := Date2JD(utc.In(zone))
// JD 约 2.5e6 天,双精度下 8 小时之差本身带 ~1e-8 h 的表示误差。
if offset := (localJD - utcJD) * 24; math.Abs(offset-8) > 1e-6 {
t.Fatalf("当地民用时框架与 UTC 相差 %.9f h,期望 8 h", offset)
}
for _, site := range []struct{ lon, lat float64 }{{108.729, -59.937}, {0, 0}, {-70, 45}} {
local := HMoonHeight(localJD, site.lon, site.lat, 8)
reference := HMoonHeight(utcJD, site.lon, site.lat, 0)
if math.Abs(local-reference) > 1e-5 {
t.Fatalf("(%.3f,%.3f) 当地民用时 %.9f,UTC 数值配 tz=0 %.9f", site.lon, site.lat, local, reference)
}
}
state := MoonStateAt(utcJD)
for _, site := range []struct{ lon, lat float64 }{{108.729, -59.937}, {0, 0}, {-70, 45}} {
if state.HMoonHeight(site.lon, site.lat) != HMoonHeight(utcJD, site.lon, site.lat, 0) {
t.Fatalf("(%.3f,%.3f) MoonState 与 HMoonHeight 不一致", site.lon, site.lat)
}
}
if mixed := HMoonHeight(utcJD, 0, 0, 8) - HMoonHeight(utcJD, 0, 0, 0); math.Abs(mixed) < 0.1 {
t.Fatalf("UTC 数值配非零 tz 的差异只有 %.6f°,用例失去区分度", mixed)
}
}
+335
View File
@@ -0,0 +1,335 @@
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 {
// JD 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JD 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 := UTC2TT(Date2JD(startUTC))
endTT := UTC2TT(Date2JD(endUTC))
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 := JD2DateByZone(event.JD, 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].JD < events[j].JD
})
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{
JD: TT2UTC(eventTT),
Declination: declination,
}
}
func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficients, direction int, includeCurrent bool) DeclinationEvent {
if !finite(jd) {
// 非有限查询算不出周期序号(centerK 会把 NaN/±Inf 压成 MinInt64,种子外推到 1e40 后步长小于一个 ULP)。
return DeclinationEvent{}
}
cfg := moonMaximumDeclinationSearchConfig(coeffs)
centerK := moonMaximumDeclinationOffset(jd, coeffs)
step := 1
if direction < 0 {
step = -1
}
event := moonMaximumDeclinationEvent(centerK, coeffs, cfg)
// 事件时刻随周期序号单调递增:该方向上最近的候选就是 centerK 沿该方向第一个满足方向不变量
// 的周期;命中后只需再回退检查更早的周期是否同样满足,可达范围仍旧是 ±3 个周期。
if !moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
event = moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg)
if moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
return event
}
}
return DeclinationEvent{}
}
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
previous := moonMaximumDeclinationEvent(centerK-step*offset, coeffs, cfg)
if !moonMaximumDeclinationMatchesDirection(previous.JD-jd, direction, includeCurrent) {
break
}
event = previous
}
return event
}
func moonClosestMaximumDeclination(jd float64, coeffs moonMaxDeclinationCoefficients) DeclinationEvent {
if !finite(jd) {
return DeclinationEvent{}
}
cfg := moonMaximumDeclinationSearchConfig(coeffs)
centerK := moonMaximumDeclinationOffset(jd, coeffs)
center := moonMaximumDeclinationEvent(centerK, coeffs, cfg)
var last, next DeclinationEvent
if center.JD <= jd {
last = center
next = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, 1, false)
} else {
next = center
last = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, -1, true)
}
lastDistance := math.Abs(jd - last.JD)
nextDistance := math.Abs(next.JD - jd)
if lastDistance <= nextDistance {
return last
}
return next
}
// moonMaximumDeclinationAround 从 centerK 沿 step 方向找第一个满足方向不变量的周期;±3 个周期内没有就返回零事件。
func moonMaximumDeclinationAround(jd float64, coeffs moonMaxDeclinationCoefficients, cfg apsisSearchConfig,
centerK, step int, includeCurrent bool) DeclinationEvent {
direction := 1
if step < 0 {
direction = -1
}
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
event := moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg)
if moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
return event
}
}
return DeclinationEvent{}
}
func moonMaximumDeclinationSearchConfig(coeffs moonMaxDeclinationCoefficients) apsisSearchConfig {
return apsisSearchConfig{
bracketHalfWidth: moonApsisBracketHalfWidth,
sampleStep: moonApsisSampleStep,
derivativeStep: moonApsisDerivativeStep,
toleranceDays: moonApsisToleranceDays,
maxIterations: moonApsisMaxIterations,
maximize: coeffs.sign > 0,
}
}
// moonMaximumDeclinationOffset 查询时刻落在哪个平均周期(种子多项式的中心序号)。
func moonMaximumDeclinationOffset(jd float64, coeffs moonMaxDeclinationCoefficients) int {
return int(math.Round((UTC2TT(jd) - coeffs.JDE0) / moonMaxDeclinationMeanMonthDays))
}
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.JD < b.JD
}
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)
}
+91
View File
@@ -0,0 +1,91 @@
package basic
import (
"math"
"testing"
)
// 本文件用改动前的“七个候选全精修”实现做差分对照与 A/B 基准。
func moonMaximumDeclinationReferenceSearch(jd float64, coeffs moonMaxDeclinationCoefficients, direction int, includeCurrent bool) DeclinationEvent {
if !finite(jd) {
return DeclinationEvent{}
}
cfg := moonMaximumDeclinationSearchConfig(coeffs)
centerK := moonMaximumDeclinationOffset(jd, coeffs)
found := false
bestDistance := math.Inf(1)
var best DeclinationEvent
for offset := -moonMaxDeclinationSearchSpan; offset <= moonMaxDeclinationSearchSpan; offset++ {
event := moonMaximumDeclinationEvent(centerK+offset, coeffs, cfg)
delta := event.JD - 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 moonMaximumDeclinationReferenceClosest(jd float64, coeffs moonMaxDeclinationCoefficients) DeclinationEvent {
if !finite(jd) {
return DeclinationEvent{}
}
last := moonMaximumDeclinationReferenceSearch(jd, coeffs, -1, true)
next := moonMaximumDeclinationReferenceSearch(jd, coeffs, 1, false)
lastDistance := math.Abs(jd - last.JD)
nextDistance := math.Abs(next.JD - jd)
if lastDistance <= nextDistance {
return last
}
return next
}
func TestMoonMaximumDeclinationPruningMatchesFullSearch(t *testing.T) {
for _, coeffs := range []moonMaxDeclinationCoefficients{moonMaxDeclinationNorthCoefficients, moonMaxDeclinationSouthCoefficients} {
for jd := JDCalc(2024, 1, 1); jd <= JDCalc(2025, 6, 1); jd += 2.5 {
for _, direction := range []struct {
name string
dir int
includeCurrent bool
fn func(float64) DeclinationEvent
}{
{"Last", -1, true, func(v float64) DeclinationEvent {
return moonMaximumDeclinationSearch(v, coeffs, -1, true)
}},
{"Next", 1, false, func(v float64) DeclinationEvent {
return moonMaximumDeclinationSearch(v, coeffs, 1, false)
}},
} {
want := moonMaximumDeclinationReferenceSearch(jd, coeffs, direction.dir, direction.includeCurrent)
got := direction.fn(jd)
if got != want {
t.Fatalf("k=%v %s(%v) = %+v, want %+v", coeffs.sign, direction.name, jd, got, want)
}
}
wantClosest := moonMaximumDeclinationReferenceClosest(jd, coeffs)
if got := moonClosestMaximumDeclination(jd, coeffs); got != wantClosest {
t.Fatalf("k=%v Closest(%v) = %+v, want %+v", coeffs.sign, jd, got, wantClosest)
}
}
}
}
func BenchmarkMoonMaximumDeclinationFullSearchReference(b *testing.B) {
jd := JDCalc(2025, 3, 1)
b.Run("Next", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = moonMaximumDeclinationReferenceSearch(jd, moonMaxDeclinationNorthCoefficients, 1, false)
}
})
b.Run("Closest", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = moonMaximumDeclinationReferenceClosest(jd, moonMaxDeclinationNorthCoefficients)
}
})
}
+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 := JD2DateByZone(got.JD, 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].JD < event.JD) {
t.Fatalf("north events not strictly increasing: %.12f then %.12f", north[i-1].JD, event.JD)
}
}
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].JD < event.JD) {
t.Fatalf("south events not strictly increasing: %.12f then %.12f", south[i-1].JD, event.JD)
}
}
}
func TestMoonMaximumDeclinationSearchMatchesMonthlyEvents(t *testing.T) {
query := time.Date(2026, time.January, 10, 0, 0, 0, 0, time.UTC)
queryJD := Date2JD(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(queryJD), expectedDirectionalDeclinationEvent(northEvents, queryJD, -1, true))
assertSameDeclinationEvent(t, "next north", NextMoonMaximumNorthDeclination(queryJD), expectedDirectionalDeclinationEvent(northEvents, queryJD, 1, false))
assertSameDeclinationEvent(t, "closest north", ClosestMoonMaximumNorthDeclination(queryJD), expectedClosestDeclinationEvent(northEvents, queryJD))
assertSameDeclinationEvent(t, "last south", LastMoonMaximumSouthDeclination(queryJD), expectedDirectionalDeclinationEvent(southEvents, queryJD, -1, true))
assertSameDeclinationEvent(t, "next south", NextMoonMaximumSouthDeclination(queryJD), expectedDirectionalDeclinationEvent(southEvents, queryJD, 1, false))
assertSameDeclinationEvent(t, "closest south", ClosestMoonMaximumSouthDeclination(queryJD), expectedClosestDeclinationEvent(southEvents, queryJD))
}
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))
}
exactJD := north[0].JD
assertSameDeclinationEvent(t, "exact last north", LastMoonMaximumNorthDeclination(exactJD), north[0])
assertSameDeclinationEvent(t, "exact closest north", ClosestMoonMaximumNorthDeclination(exactJD), north[0])
assertSameDeclinationEvent(t, "exact next north", NextMoonMaximumNorthDeclination(exactJD), north[1])
}
func assertSameDeclinationEvent(t *testing.T, name string, got, want DeclinationEvent) {
t.Helper()
if math.Abs(got.JD-want.JD) > 1e-12 {
t.Fatalf("%s JD mismatch: got %.12f want %.12f", name, got.JD, want.JD)
}
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, queryJD float64, direction int, includeCurrent bool) DeclinationEvent {
var (
found bool
best DeclinationEvent
)
for _, event := range events {
delta := event.JD - queryJD
if !moonMaximumDeclinationMatchesDirection(delta, direction, includeCurrent) {
continue
}
if !found {
best = event
found = true
continue
}
if math.Abs(delta) < math.Abs(best.JD-queryJD) || (math.Abs(delta) == math.Abs(best.JD-queryJD) && event.JD < best.JD) {
best = event
}
}
return best
}
func expectedClosestDeclinationEvent(events []DeclinationEvent, queryJD float64) DeclinationEvent {
last := expectedDirectionalDeclinationEvent(events, queryJD, -1, true)
next := expectedDirectionalDeclinationEvent(events, queryJD, 1, false)
if math.Abs(queryJD-last.JD) <= math.Abs(next.JD-queryJD) {
return last
}
return next
}
+485
View File
@@ -0,0 +1,485 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
/*
* 月球方位角
*/
func MoonAzimuth(jd, lon, lat, tz float64) float64 {
//tmp := (tz*15 - lon) * 4 / 60
jde := UTC2TT(jd - tz/24)
ra := MoonTrueRa(jde)
dec := MoonTrueDec(jde)
away := MoonAway(jde) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*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;
jde := UTC2TT(jd - tz/24)
ra := MoonTrueRa(jde)
dec := MoonTrueDec(jde)
away := MoonAway(jde) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*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 {
jde := UTC2TT(jd - tz/24)
ra := HMoonTrueRaN(jde, n)
dec := HMoonTrueDecN(jde, n)
away := HMoonAwayN(jde, 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)
jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*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
}
}
}
// HMoonHeight 当地民用时儒略日下的月心几何高度角(度,不含折射)/ geometric Moon-centre altitude in degrees for a local civil Julian day.
//
// jd 是该时区的当地民用时(墙上时刻)儒略日,tz 是时区偏移小时数,库内按 jd−tz/24 换成 UTC。
// 只有 tz 给 0 时 jd 才是 UTC 儒略日;不要拿 UTC 数值再配非零 tz,那会多减一次时区。
// jd is that zone's local civil (wall-clock) Julian day and tz is the zone offset in hours,
// converted internally as jd-tz/24. Only tz 0 makes jd a UTC Julian day: pairing a UTC value with a
// non-zero tz subtracts the offset twice.
func HMoonHeight(jd, lon, lat, tz float64) float64 {
return HMoonHeightN(jd, lon, lat, tz, -1)
}
type moonObservationState struct {
altitude float64
distanceKM float64
}
func hMoonObservationStateN(jd, lon, lat, tz, height float64, n int) moonObservationState {
calculationJDE := UTC2TT(jd - tz/24)
ra, dec := HMoonTrueRaDecN(calculationJDE, n)
distanceKM := HMoonAwayN(calculationJDE, n)
distanceAU := distanceKM / angularDiameterAstronomicalUnitKM
topocentricRA, topocentricDec := TopocentricRaDec(ra, dec, lat, lon, jd-tz/24, distanceAU, height)
siderealTime := Limit360(ApparentSiderealTime(UTC2UT1(jd-tz/24))*15 + lon)
hourAngle := Limit360(siderealTime - topocentricRA)
altitudeSine := Sin(lat)*Sin(topocentricDec) + Cos(topocentricDec)*Cos(lat)*Cos(hourAngle)
return moonObservationState{
altitude: ArcSin(altitudeSine),
distanceKM: distanceKM,
}
}
func HMoonHeightN(jd, lon, lat, tz float64, n int) float64 {
return hMoonObservationStateN(jd, lon, lat, tz, 0, n).altitude
}
// MoonState 同一瞬间可对任意观测点复用的月球位置与恒星时 / one instant's lunar position and sidereal time, reusable across observers.
type MoonState struct {
rightAscension float64
declination float64
distanceAU float64
siderealTime float64
}
// MoonStateAt 由 UTC 儒略日构造该瞬间的可复用月球状态 / builds the reusable state for one UTC Julian day.
func MoonStateAt(utcJD float64) MoonState {
jde := UTC2TT(utcJD)
rightAscension, declination := HMoonTrueRaDec(jde)
return MoonState{
rightAscension: rightAscension,
declination: declination,
distanceAU: HMoonAway(jde) / angularDiameterAstronomicalUnitKM,
siderealTime: ApparentSiderealTime(UTC2UT1(utcJD)) * 15,
}
}
func (state MoonState) finite() bool {
return finite(state.rightAscension) && finite(state.declination) &&
finite(state.distanceAU) && finite(state.siderealTime)
}
// HMoonHeight 给定观测者经度、纬度(度,椭球高 0)的月心几何高度角,等于 HMoonHeight(构造本状态时的 UTC 儒略日, 经, 纬, 0)。
// HMoonHeight returns the geometric Moon-centre altitude for one observer, equal to HMoonHeight(the UTC Julian day given to MoonStateAt, lon, lat, 0).
func (state MoonState) HMoonHeight(longitude, latitude float64) float64 {
// 本状态固定是 UTC 瞬间、椭球高 0,因此只对应包级 tz=0、height=0 的用法。
// 恒星时已在状态里算好,这里不再走会重算恒星时与时标换算的 TopocentricRaDec。
topocentricRA, topocentricDec := topocentricRaDecWithSidereal(
state.rightAscension, state.declination, latitude, longitude, state.siderealTime, state.distanceAU, 0,
)
hourAngle := Limit360(Limit360(state.siderealTime+longitude) - topocentricRA)
return ArcSin(Sin(latitude)*Sin(topocentricDec) + Cos(topocentricDec)*Cos(latitude)*Cos(hourAngle))
}
// MoonHorizon 用本状态生成海平面几何月心地平圈,口径同包级 MoonHorizon / sea-level geometric Moon-centre horizon ring from this state.
func (state MoonState) MoonHorizon(samples int) [][2]float64 {
if !state.finite() {
return nil
}
if samples <= 0 {
samples = 360
}
if samples < 12 {
samples = 12
} else if samples > 1440 {
samples = 1440
}
parallax := math.Sin(0.0024427777777*rad) / state.distanceAU
longitude := (state.rightAscension - state.siderealTime) * rad
latitude := state.declination * rad
if !finite(parallax) || parallax <= 0 || parallax >= 1 || !finite(longitude) || !finite(latitude) {
return nil
}
center := [3]float64{math.Cos(latitude) * math.Cos(longitude), math.Cos(latitude) * math.Sin(longitude), math.Sin(latitude)}
north := [3]float64{-math.Sin(latitude) * math.Cos(longitude), -math.Sin(latitude) * math.Sin(longitude), math.Cos(latitude)}
east := [3]float64{-math.Sin(longitude), math.Cos(longitude), 0}
points := make([][2]float64, samples)
for index := range points {
bearing := 2 * math.Pi * float64(index) / float64(samples)
radius := math.Acos(parallax)
var point [3]float64
for iteration := 0; iteration < 8; iteration++ {
for axis := range point {
point[axis] = center[axis]*math.Cos(radius) +
(north[axis]*math.Cos(bearing)+east[axis]*math.Sin(bearing))*math.Sin(radius)
}
lat := math.Asin(math.Max(-1, math.Min(1, point[2]))) / rad
// The topocentric direction is horizontal when its dot product
// with the geodetic zenith vanishes: cos(radius)=observer/range.
next := math.Acos(parallax * (pcosi(lat, 0)*math.Cos(lat*rad) + psini(lat, 0)*math.Sin(lat*rad)))
if math.Abs(next-radius) < 1e-14 {
break
}
radius = next
}
points[index] = [2]float64{math.Atan2(point[1], point[0]) / rad, math.Asin(math.Max(-1, math.Min(1, point[2]))) / rad}
}
return points
}
func moonRiseSetResidual(jd, longitude, latitude, timeZone, zenithShift, height float64, n int) float64 {
state := hMoonObservationStateN(jd, longitude, latitude, timeZone, height, n)
// 相对观测者下沉地平线的视上缘高度角 / Apparent upper-limb altitude relative to the observer's depressed horizon.
residual := state.altitude + HeightDegreeByLat(height, latitude)
if zenithShift != 0 {
residual += RefractionFromTrueAltitude(state.altitude, refractionStandardPressureHPa, refractionStandardTemperatureC)
residual += angularSemidiameterArcsec(moonEquatorialRadiusKM, state.distanceKM) / 3600
}
return residual
}
// moonRiseSetOnCivilDay 在民用日内求升/落时刻;找不到过零时的错误口径与 rise_set.go 的 ErrNeverRise/ErrNeverSet 一致,
// fallbackErr 是调用方用中天/下中天残差预判的同一几何结论,命中时优先于扫描结果。
func moonRiseSetOnCivilDay(candidate, slope, civilDayStart, longitude, latitude, originalTimeZone,
localTimeZone, zenithShift, height float64, isRise bool, fallbackErr error) (float64, error) {
if eventRiseSetCandidateValid(candidate, civilDayStart, slope, isRise) {
return candidate, nil
}
return eventDirectionalRiseSetSearch(civilDayStart, isRise, fallbackErr, func(outputJD float64) float64 {
localJD := outputJD + localTimeZone/24 - originalTimeZone/24
return moonRiseSetResidual(localJD, longitude, latitude, localTimeZone, zenithShift, height, -1)
})
}
// 废弃
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
var ok bool
estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 {
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
return stDegree / stDegreep
})
if !ok {
return math.NaN()
}
return estimateJD
}
func MoonCulminationTime(localJD, lon, lat, timezone float64) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时;ra/dec 为瞬时天球坐标,非 J2000 等固定历元。
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-MoonTimeAngle(localJD, lon, lat, timezone))/15.0/24.0/0.9
limitHA := func(localJD, lon, timezone float64) float64 {
ha := MoonTimeAngle(localJD, lon, lat, timezone)
if ha < 180 {
ha += 360
}
return ha
}
var ok bool
estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 {
stDegree := limitHA(prevJD, lon, timezone) - 360
stDegreep := (limitHA(prevJD+0.000005, lon, timezone) - limitHA(prevJD-0.000005, lon, timezone)) / 0.00001
return stDegree / stDegreep
})
if !ok {
return math.NaN()
}
return estimateJD
}
func MoonTimeAngle(jd, lon, lat, tz float64) float64 {
startime := Limit360(ApparentSiderealTime(UTC2UT1(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) {
if !isFiniteFloat(julianDay) || !isFiniteFloat(longitude) || !isFiniteFloat(latitude) || !isFiniteFloat(timeZone) || !isFiniteFloat(zenithShift) || !isFiniteFloat(height) {
return 0, ErrInvalidObservationInput
}
originalTimeZone := timeZone
timeZone = longitude / 15
var timeToMeridian float64
civilDayStart := math.Floor(julianDay) + 0.5
// 时间分界线以传入的时区为准,不用当地时区,否则 0 时的判断会出错。
julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1)
moonAngle := StandardAltitudeMoon(zenithShift, height, latitude)
moonAngleTime := MoonTimeAngle(julianDay, longitude, latitude, originalTimeZone)
if moonResidual > 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 moonResidual < 0 && moonAngleTime > 180 {
timeToMeridian = (180 - moonAngleTime) / 15
estimatedTime += (timeToMeridian/24 + (timeToMeridian/24*12.0)/15.0/24.0)
} else if moonResidual < 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
}
currentResidual := moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1)
if !(currentResidual < -10 && math.Abs(latitude) < 60) {
if currentResidual > 0 {
// 下中天仍在地平线上:当日无落下(也无可升起),口径见 moonRiseSetOnCivilDay。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, true, 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 moonRiseSetResidual(checkTime, longitude, latitude, timeZone, zenithShift, height, -1) < 0 {
// 上中天仍在地平线下:当日无升起。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, true, 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 moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1) < 0 {
i++
estimatedTime += 15.0 / 60.0 / 24.0
if i > 48 {
break
}
}
}
// 使用牛顿迭代法求精确解
estimatedTime, slope := moonRiseSetResidualIteration(estimatedTime, longitude, latitude, timeZone, zenithShift, height, 0.00002)
estimatedTime = estimatedTime - timeZone/24 + originalTimeZone/24
return moonRiseSetOnCivilDay(estimatedTime, slope, civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, true, nil)
}
func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) {
if !isFiniteFloat(julianDay) || !isFiniteFloat(longitude) || !isFiniteFloat(latitude) || !isFiniteFloat(timeZone) || !isFiniteFloat(zenithShift) || !isFiniteFloat(height) {
return 0, ErrInvalidObservationInput
}
originalTimeZone := timeZone
timeZone = longitude / 15
var timeToMeridian float64
civilDayStart := math.Floor(julianDay) + 0.5
// 时间分界线以传入的时区为准,不用当地时区,否则 0 时的判断会出错。
julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1)
moonAngle := StandardAltitudeMoon(zenithShift, height, latitude)
moonAngleTime := MoonTimeAngle(julianDay, longitude, latitude, originalTimeZone)
if moonResidual < 0 {
timeToMeridian = (360 - moonAngleTime) / 15
estimatedTime += (timeToMeridian/24 + (timeToMeridian/24.0*12.0)/15.0/24.0)
}
// 月亮在地平线上或在落下与下中天之间
if moonResidual > 0 && moonAngleTime < 180 {
timeToMeridian = (-moonAngleTime) / 15
estimatedTime += (timeToMeridian/24.0 + (timeToMeridian/24.0*12.0)/15.0/24.0)
} else if moonResidual > 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 = 月球中天时间
currentResidual := moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1)
if !(currentResidual > 10 && math.Abs(latitude) < 60) {
if currentResidual < 0 {
// 上中天仍在地平线下:当日无升起,也就无落下。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, false, 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 moonRiseSetResidual(checkTime, longitude, latitude, timeZone, zenithShift, height, -1) > 0 {
// 下中天仍在地平线上:当日无落下。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, false, 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 moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1) > 0 {
i++
estimatedTime += 15.0 / 60.0 / 24.0
if i > 48 {
break
}
}
}
// 使用牛顿迭代法求精确解
estimatedTime, slope := moonRiseSetResidualIteration(estimatedTime, longitude, latitude, timeZone, zenithShift, height, 0.00002)
estimatedTime = estimatedTime - timeZone/24 + originalTimeZone/24
return moonRiseSetOnCivilDay(estimatedTime, slope, civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, false, 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
var ok bool
currentTime, ok = eventNewtonRefine(currentTime, tolerance, func(previousTime float64) float64 {
functionValue := heightFunc(previousTime, longitude, latitude, timeZone) - targetAngle
derivative := (heightFunc(previousTime+derivativeStep, longitude, latitude, timeZone) -
heightFunc(previousTime-derivativeStep, longitude, latitude, timeZone)) / (2 * derivativeStep)
return functionValue / derivative
})
if !ok {
return math.NaN()
}
return currentTime
}
func moonRiseSetResidualIteration(initialTime, longitude, latitude, timeZone, zenithShift, height, tolerance float64) (float64, float64) {
const derivativeStep = 0.000005
slope := math.NaN()
currentTime, ok := eventNewtonRefine(initialTime, tolerance, func(previousTime float64) float64 {
functionValue := moonRiseSetResidual(previousTime, longitude, latitude, timeZone, zenithShift, height, -1)
slope = (moonRiseSetResidual(previousTime+derivativeStep, longitude, latitude, timeZone, zenithShift, height, -1) -
moonRiseSetResidual(previousTime-derivativeStep, longitude, latitude, timeZone, zenithShift, height, -1)) / (2 * derivativeStep)
return functionValue / slope
})
if !ok {
return math.NaN(), math.NaN()
}
return currentTime, slope
}
+207
View File
@@ -0,0 +1,207 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
func MoonPhase(jde float64) float64 {
moonBo := HMoonTrueBo(jde)
sunLo := HSunApparentLo(jde)
moonLo := HMoonApparentLo(jde)
tmp := Cos(moonBo) * Cos(sunLo-moonLo)
earthSunDistance := Distance(jde) * 149597870.691
i := earthSunDistance * Sin(ArcCos(tmp)) / (HMoonAway(jde) - 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
var ok bool
estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 {
stDegree := SunMoonSeek(prevJD, float64(phaseType))
stDegreep := (SunMoonSeek(prevJD+0.000005, float64(phaseType)) - SunMoonSeek(prevJD-0.000005, float64(phaseType))) / 0.00001
return stDegree / stDegreep
})
if !ok {
return math.NaN()
}
return estimateJD
}
func CalcMoonSH(year float64, phaseType int) float64 {
jde := CalcMoonS(year, phaseType)
phaseType = phaseType * 180
estimateJD := jde
var ok bool
estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 {
stDegree := SunMoonSeek(prevJD, float64(phaseType))
stDegreep := (SunMoonSeek(prevJD+0.000005, float64(phaseType)) - SunMoonSeek(prevJD-0.000005, float64(phaseType))) / 0.00001
return stDegree / stDegreep
})
if !ok {
return math.NaN()
}
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
var ok bool
estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 {
stDegree := SunMoonSeek(prevJD, float64(quarterType))
stDegreep := (SunMoonSeek(prevJD+0.000005, float64(quarterType)) - SunMoonSeek(prevJD-0.000005, float64(quarterType))) / 0.00001
return stDegree / stDegreep
})
if !ok {
return math.NaN()
}
return estimateJD
}
func CalcMoonXH(year float64, quarterType int) float64 {
jde := CalcMoonX(year, quarterType)
if quarterType == 0 {
quarterType = 90
} else {
quarterType = -90
}
estimateJD := jde
var ok bool
estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 {
stDegree := SunMoonSeek(prevJD, float64(quarterType))
stDegreep := (SunMoonSeek(prevJD+0.000005, float64(quarterType)) - SunMoonSeek(prevJD-0.000005, float64(quarterType))) / 0.00001
return stDegree / stDegreep
})
if !ok {
return math.NaN()
}
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
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(jde float64) MoonPhysicalInfo {
return MoonPhysicalN(jde, -1)
}
// MoonPhysicalN 月球物理观测参数(截断版) / truncated physical observing parameters of the Moon.
func MoonPhysicalN(jde float64, n int) MoonPhysicalInfo {
return moonPhysicalNFromCoordinates(jde, n, HMoonApparentLoN(jde, n), HMoonTrueBoN(jde, n), HMoonTrueRaN(jde, n))
}
// MoonTopocentricPhysical 月球站心物理观测参数 / topocentric physical observing parameters of the Moon.
func MoonTopocentricPhysical(jde, observerLon, observerLat, height float64) MoonPhysicalInfo {
return MoonTopocentricPhysicalN(jde, observerLon, observerLat, height, -1)
}
// MoonTopocentricPhysicalN 月球站心物理观测参数(截断版) / truncated topocentric physical observing parameters of the Moon.
func MoonTopocentricPhysicalN(jde, observerLon, observerLat, height float64, n int) MoonPhysicalInfo {
lambda, beta, alpha := moonTopocentricPhysicalCoordinatesN(jde, observerLon, observerLat, height, n)
return moonPhysicalNFromCoordinates(jde, n, lambda, beta, alpha)
}
func moonPhysicalNFromCoordinates(jde float64, n int, lambda, beta, alpha float64) MoonPhysicalInfo {
t := (jde - 2451545.0) / 36525.0
epsilon := TrueObliquity(jde)
deltaPsi := Nutation2000Bi(jde)
D := Limit360(SunMoonAngle(jde))
sunMeanAnomaly := Limit360(SunM(jde))
moonMeanAnomaly := Limit360(MoonM(jde))
F := Limit360(MoonLonX(jde))
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(jde, observerLon, observerLat, height float64, n int) (lambda, beta, alpha float64) {
geocentricRA := HMoonTrueRaN(jde, n)
geocentricDec := HMoonTrueDecN(jde, n)
distanceAU := HMoonAwayN(jde, n) / moonPhysicalAstronomicalUnitKM
utcJD := TT2UTC(jde)
var topocentricDec float64
alpha, topocentricDec = TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, utcJD, distanceAU, height)
lambda, beta = RaDecToLoBo(jde, 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 := UTC2TT(Date2JD(date.UTC()))
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)
}
}
+397
View File
@@ -0,0 +1,397 @@
package basic
import "math"
const (
moonPlanetConjunctionEstimateN = 8
moonPlanetConjunctionNearQueryDeltaDeg = 3.0
moonPlanetConjunctionDirectionEpsilon = 0.1 / 86400.0
moonPlanetConjunctionBracketStepDays = 0.5
moonPlanetConjunctionNearQueryStepDays = 0.25
moonPlanetConjunctionNearQueryHalfSpan = 1.5
moonPlanetConjunctionBracketHalfSpan = 2.0
moonPlanetConjunctionBracketGrowth = 2.0
moonPlanetConjunctionBracketAttempts = 3
moonPlanetConjunctionRefineStepDays = 0.5 / 86400.0
moonPlanetConjunctionEventTolerance = 0.01
moonPlanetConjunctionFallbackSpanScale = 1.5
)
type moonPlanetConjunctionLocalResult struct {
lastUT float64
nextUT float64
}
func emptyMoonPlanetConjunctionLocalResult() moonPlanetConjunctionLocalResult {
return moonPlanetConjunctionLocalResult{
lastUT: math.NaN(),
nextUT: math.NaN(),
}
}
// MoonPlanetConjunctionPlanet 月球合月目标行星 / target planet for Moon-planet conjunction events.
type MoonPlanetConjunctionPlanet int
const (
MoonPlanetConjunctionMercury MoonPlanetConjunctionPlanet = iota + 1
MoonPlanetConjunctionVenus
MoonPlanetConjunctionMars
MoonPlanetConjunctionJupiter
MoonPlanetConjunctionSaturn
MoonPlanetConjunctionUranus
MoonPlanetConjunctionNeptune
)
func moonPlanetConjunctionWrappedDelta(diff float64) float64 {
diff = math.Mod(diff+180, 360)
if diff < 0 {
diff += 360
}
return diff - 180
}
func moonPlanetConjunctionDeltaAt(jdTT float64, planet MoonPlanetConjunctionPlanet, n int) float64 {
moonRA := HMoonGeocentricApparentRaN(jdTT, n)
var planetRA float64
switch planet {
case MoonPlanetConjunctionMercury:
planetRA = MercuryApparentRaN(jdTT, n)
case MoonPlanetConjunctionVenus:
planetRA = VenusApparentRaN(jdTT, n)
case MoonPlanetConjunctionMars:
planetRA = MarsApparentRaN(jdTT, n)
case MoonPlanetConjunctionJupiter:
planetRA = JupiterApparentRaN(jdTT, n)
case MoonPlanetConjunctionSaturn:
planetRA = SaturnApparentRaN(jdTT, n)
case MoonPlanetConjunctionUranus:
planetRA = UranusApparentRaN(jdTT, n)
case MoonPlanetConjunctionNeptune:
planetRA = NeptuneApparentRaN(jdTT, n)
default:
return math.NaN()
}
return moonPlanetConjunctionWrappedDelta(moonRA - planetRA)
}
func moonPlanetConjunctionPeriodDays(planet MoonPlanetConjunctionPlanet) float64 {
switch planet {
case MoonPlanetConjunctionMercury:
return 28.1
case MoonPlanetConjunctionVenus:
return 28.4
case MoonPlanetConjunctionMars:
return 29.2
case MoonPlanetConjunctionJupiter:
return 28.0
case MoonPlanetConjunctionSaturn:
return 27.4
case MoonPlanetConjunctionUranus:
return 27.3
case MoonPlanetConjunctionNeptune:
return 27.3
default:
return math.NaN()
}
}
func moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) <= moonPlanetConjunctionDirectionEpsilon
}
func moonPlanetConjunctionAfterOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) >= -moonPlanetConjunctionDirectionEpsilon
}
func moonPlanetConjunctionInDirection(eventUT, queryTT float64, direction int) bool {
switch direction {
case -1:
return moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT)
case 1:
return moonPlanetConjunctionAfterOrEqual(eventUT, queryTT)
default:
return true
}
}
func moonPlanetConjunctionFindBracket(centerTT, halfSpan, step float64, planet MoonPlanetConjunctionPlanet) (float64, float64, bool) {
if math.IsNaN(centerTT) || math.IsNaN(halfSpan) || math.IsNaN(step) || halfSpan <= 0 || step <= 0 {
return 0, 0, false
}
start := centerTT - halfSpan
end := centerTT + halfSpan
samples := int(math.Ceil((end-start)/step)) + 1
prevTT := start
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1)
if math.IsNaN(prevVal) {
return 0, 0, false
}
if prevVal == 0 {
return prevTT, prevTT, true
}
bestLeft := 0.0
bestRight := 0.0
bestDistance := math.Inf(1)
for i := 1; i <= samples; i++ {
tt := start + float64(i)*step
if tt > end {
tt = end
}
val := moonPlanetConjunctionDeltaAt(tt, planet, -1)
if math.IsNaN(val) {
return 0, 0, false
}
if val == 0 {
return tt, tt, true
}
if prevVal*val < 0 {
mid := (prevTT + tt) / 2.0
distance := math.Abs(mid - centerTT)
if distance < bestDistance {
bestLeft = prevTT
bestRight = tt
bestDistance = distance
}
}
if tt == end {
break
}
prevTT = tt
prevVal = val
}
if math.IsInf(bestDistance, 1) {
return 0, 0, false
}
return bestLeft, bestRight, true
}
func moonPlanetConjunctionRefineBracket(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet) float64 {
if leftTT > rightTT {
leftTT, rightTT = rightTT, leftTT
}
if leftTT == rightTT {
return leftTT
}
center := (leftTT + rightTT) / 2.0
halfWindow := (rightTT - leftTT) / 2.0
return eventZeroRefine(center, halfWindow, moonPlanetConjunctionRefineStepDays, func(sampleTT float64) float64 {
return moonPlanetConjunctionDeltaAt(sampleTT, planet, -1)
})
}
func moonPlanetConjunctionEventUT(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet) float64 {
eventTT := moonPlanetConjunctionRefineBracket(leftTT, rightTT, planet)
if math.Abs(moonPlanetConjunctionDeltaAt(eventTT, planet, -1)) > moonPlanetConjunctionEventTolerance {
return math.NaN()
}
return TT2UTC(eventTT)
}
func moonPlanetConjunctionCollectLocalEvent(result *moonPlanetConjunctionLocalResult, queryTT, eventUT float64) {
if math.IsNaN(eventUT) {
return
}
if moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT) {
if math.IsNaN(result.lastUT) || math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) < math.Abs(eventUTQueryTTDelta(result.lastUT, queryTT)) {
result.lastUT = eventUT
}
}
if moonPlanetConjunctionAfterOrEqual(eventUT, queryTT) {
if math.IsNaN(result.nextUT) || math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) < math.Abs(eventUTQueryTTDelta(result.nextUT, queryTT)) {
result.nextUT = eventUT
}
}
}
func moonPlanetConjunctionShouldCheckLocal(queryTT float64, planet MoonPlanetConjunctionPlanet) bool {
delta := moonPlanetConjunctionDeltaAt(queryTT, planet, moonPlanetConjunctionEstimateN)
if math.IsNaN(delta) {
return false
}
return math.Abs(delta) <= moonPlanetConjunctionNearQueryDeltaDeg
}
func moonPlanetConjunctionLocalEvents(queryTT float64, planet MoonPlanetConjunctionPlanet) moonPlanetConjunctionLocalResult {
result := emptyMoonPlanetConjunctionLocalResult()
start := queryTT - moonPlanetConjunctionNearQueryHalfSpan
end := queryTT + moonPlanetConjunctionNearQueryHalfSpan
step := moonPlanetConjunctionNearQueryStepDays
prevTT := start
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1)
if math.IsNaN(prevVal) {
return result
}
samples := int(math.Ceil((end-start)/step)) + 1
for i := 1; i <= samples; i++ {
tt := start + float64(i)*step
if tt > end {
tt = end
}
val := moonPlanetConjunctionDeltaAt(tt, planet, -1)
if math.IsNaN(val) {
return emptyMoonPlanetConjunctionLocalResult()
}
if prevVal == 0 || val == 0 || prevVal*val < 0 {
moonPlanetConjunctionCollectLocalEvent(&result, queryTT, moonPlanetConjunctionEventUT(prevTT, tt, planet))
}
if tt == end {
break
}
prevTT = tt
prevVal = val
}
return result
}
func moonPlanetConjunctionMaybeLocalEvents(queryTT float64, planet MoonPlanetConjunctionPlanet) moonPlanetConjunctionLocalResult {
if !moonPlanetConjunctionShouldCheckLocal(queryTT, planet) {
return emptyMoonPlanetConjunctionLocalResult()
}
return moonPlanetConjunctionLocalEvents(queryTT, planet)
}
func moonPlanetConjunctionGuessTT(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 {
delta := moonPlanetConjunctionDeltaAt(queryTT, planet, moonPlanetConjunctionEstimateN)
if math.IsNaN(delta) {
return math.NaN()
}
period := moonPlanetConjunctionPeriodDays(planet)
if math.IsNaN(period) {
return math.NaN()
}
switch direction {
case -1:
return queryTT - innerLastCycleOffset(delta, period)
case 1:
return queryTT + innerNextCycleOffset(delta, period)
default:
return math.NaN()
}
}
func moonPlanetConjunctionDirectionalFallback(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 {
period := moonPlanetConjunctionPeriodDays(planet)
if math.IsNaN(period) {
return math.NaN()
}
span := period * moonPlanetConjunctionFallbackSpanScale
if span <= 0 {
return math.NaN()
}
step := moonPlanetConjunctionNearQueryStepDays
start := queryTT
end := queryTT
switch direction {
case -1:
start -= span
case 1:
end += span
default:
return math.NaN()
}
prevTT := start
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1)
if math.IsNaN(prevVal) {
return math.NaN()
}
bestEventUT := math.NaN()
for tt := start + step; ; tt += step {
if tt > end {
tt = end
}
val := moonPlanetConjunctionDeltaAt(tt, planet, -1)
if math.IsNaN(val) {
return math.NaN()
}
if prevVal == 0 || val == 0 || prevVal*val < 0 {
eventUT := moonPlanetConjunctionEventUT(prevTT, tt, planet)
if !math.IsNaN(eventUT) && moonPlanetConjunctionInDirection(eventUT, queryTT, direction) {
if direction == 1 {
return eventUT
}
bestEventUT = eventUT
}
}
if tt == end {
break
}
prevTT = tt
prevVal = val
}
return bestEventUT
}
func moonPlanetConjunctionDirectionalEventWithLocal(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int, local moonPlanetConjunctionLocalResult) float64 {
switch direction {
case -1:
if !math.IsNaN(local.lastUT) {
return local.lastUT
}
case 1:
if !math.IsNaN(local.nextUT) {
return local.nextUT
}
}
guessTT := moonPlanetConjunctionGuessTT(queryTT, planet, direction)
if math.IsNaN(guessTT) {
return math.NaN()
}
halfSpan := moonPlanetConjunctionBracketHalfSpan
for attempt := 0; attempt < moonPlanetConjunctionBracketAttempts; attempt++ {
left, right, ok := moonPlanetConjunctionFindBracket(guessTT, halfSpan, moonPlanetConjunctionBracketStepDays, planet)
if ok {
eventUT := moonPlanetConjunctionEventUT(left, right, planet)
if math.IsNaN(eventUT) {
halfSpan *= moonPlanetConjunctionBracketGrowth
continue
}
if moonPlanetConjunctionInDirection(eventUT, queryTT, direction) {
return eventUT
}
}
halfSpan *= moonPlanetConjunctionBracketGrowth
}
return moonPlanetConjunctionDirectionalFallback(queryTT, planet, direction)
}
func moonPlanetConjunctionDirectionalEvent(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 {
return moonPlanetConjunctionDirectionalEventWithLocal(queryTT, planet, direction, moonPlanetConjunctionMaybeLocalEvents(queryTT, planet))
}
// LastMoonPlanetConjunction 指定时刻之前最近一次行星合月(赤经合) / previous Moon-planet conjunction at or before jde.
func LastMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 {
return moonPlanetConjunctionDirectionalEvent(jde, planet, -1)
}
// NextMoonPlanetConjunction 指定时刻之后最近一次行星合月(赤经合) / next Moon-planet conjunction at or after jde.
func NextMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 {
return moonPlanetConjunctionDirectionalEvent(jde, planet, 1)
}
// ClosestMoonPlanetConjunction 离指定时刻最近一次行星合月(赤经合) / closest Moon-planet conjunction to jde.
func ClosestMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 {
local := moonPlanetConjunctionMaybeLocalEvents(jde, planet)
if !math.IsNaN(local.lastUT) && !math.IsNaN(local.nextUT) {
if sameEventJD(local.lastUT, local.nextUT) {
return local.lastUT
}
return closestEventUTToQueryTT(jde, local.lastUT, local.nextUT)
}
if !math.IsNaN(local.lastUT) {
return local.lastUT
}
if !math.IsNaN(local.nextUT) {
return local.nextUT
}
last := moonPlanetConjunctionDirectionalEventWithLocal(jde, planet, -1, local)
next := moonPlanetConjunctionDirectionalEventWithLocal(jde, planet, 1, local)
if math.IsNaN(last) {
return next
}
if math.IsNaN(next) {
return last
}
return closestEventUTToQueryTT(jde, last, next)
}
@@ -0,0 +1,284 @@
package basic
import (
"encoding/json"
"math"
"os"
"testing"
"time"
)
type moonPlanetConjunctionBaselineSample struct {
Planet string `json:"planet"`
Year int `json:"year"`
Month int `json:"month"`
TimeUTC string `json:"time_utc"`
}
type moonPlanetConjunctionBaseline struct {
Samples []moonPlanetConjunctionBaselineSample `json:"samples"`
}
func loadMoonPlanetConjunctionBaseline(t *testing.T) moonPlanetConjunctionBaseline {
t.Helper()
paths := [][]string{
{
"testdata/moon_planet_conjunction_baseline.json",
"basic/testdata/moon_planet_conjunction_baseline.json",
},
{
"testdata/moon_planet_conjunction_baseline_samples.json",
"basic/testdata/moon_planet_conjunction_baseline_samples.json",
},
}
var merged moonPlanetConjunctionBaseline
for index, candidates := range paths {
var (
data []byte
err error
)
for _, path := range candidates {
data, err = os.ReadFile(path)
if err == nil {
var baseline moonPlanetConjunctionBaseline
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatalf("decode baseline %s: %v", path, err)
}
merged.Samples = append(merged.Samples, baseline.Samples...)
break
}
}
if err != nil && index == 0 {
t.Fatalf("read baseline: %v", err)
}
}
if len(merged.Samples) == 0 {
t.Fatal("empty moon-planet conjunction baseline")
}
return merged
}
func TestMoonPlanetConjunctionsMatchHorizonsBaseline(t *testing.T) {
baseline := loadMoonPlanetConjunctionBaseline(t)
type conjunctionCase struct {
planet MoonPlanetConjunctionPlanet
next func(float64, MoonPlanetConjunctionPlanet) float64
}
cases := map[string]conjunctionCase{
"mercury": {planet: MoonPlanetConjunctionMercury, next: NextMoonPlanetConjunction},
"venus": {planet: MoonPlanetConjunctionVenus, next: NextMoonPlanetConjunction},
"mars": {planet: MoonPlanetConjunctionMars, next: NextMoonPlanetConjunction},
"jupiter": {planet: MoonPlanetConjunctionJupiter, next: NextMoonPlanetConjunction},
"saturn": {planet: MoonPlanetConjunctionSaturn, next: NextMoonPlanetConjunction},
"uranus": {planet: MoonPlanetConjunctionUranus, next: NextMoonPlanetConjunction},
"neptune": {planet: MoonPlanetConjunctionNeptune, next: NextMoonPlanetConjunction},
}
const tolerance = 20 * time.Second
var maxDiff time.Duration
seen := make(map[string]int, len(cases))
for _, sample := range baseline.Samples {
tc, ok := cases[sample.Planet]
if !ok {
t.Fatalf("unknown planet %q", sample.Planet)
}
wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
}
queryTT := UTC2TT(Date2JD(wantTime.Add(-12 * time.Hour).UTC()))
gotUT := tc.next(queryTT, tc.planet)
gotTime := JD2DateByZone(gotUT, time.UTC, false)
diff := gotTime.Sub(wantTime)
if diff < 0 {
diff = -diff
}
if diff > maxDiff {
maxDiff = diff
}
if diff > tolerance {
t.Fatalf("%s %04d-%02d time mismatch: got %s want %s tolerance %v", sample.Planet, sample.Year, sample.Month, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, tolerance)
}
delta := math.Abs(moonPlanetConjunctionDeltaAt(UTC2TT(gotUT), tc.planet, -1))
if delta > 0.01 {
t.Fatalf("%s %04d-%02d event not near conjunction: delta=%.8f deg", sample.Planet, sample.Year, sample.Month, delta)
}
seen[sample.Planet]++
}
for planet := range cases {
if seen[planet] == 0 {
t.Fatalf("missing baseline samples for %s", planet)
}
}
t.Logf("moon-planet conjunction max diff: time=%v", maxDiff)
}
func TestMoonPlanetConjunctionDirectionalConsistencyAtComputedEvent(t *testing.T) {
baseline := loadMoonPlanetConjunctionBaseline(t)
planets := map[string]MoonPlanetConjunctionPlanet{
"mercury": MoonPlanetConjunctionMercury,
"venus": MoonPlanetConjunctionVenus,
"mars": MoonPlanetConjunctionMars,
"jupiter": MoonPlanetConjunctionJupiter,
"saturn": MoonPlanetConjunctionSaturn,
"uranus": MoonPlanetConjunctionUranus,
"neptune": MoonPlanetConjunctionNeptune,
}
for _, sample := range baseline.Samples {
planet, ok := planets[sample.Planet]
if !ok {
t.Fatalf("unknown planet %q", sample.Planet)
}
wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
}
seedTT := UTC2TT(Date2JD(wantTime.Add(-12 * time.Hour).UTC()))
eventUT := NextMoonPlanetConjunction(seedTT, planet)
eventTime := JD2DateByZone(eventUT, time.UTC, false)
queryAtTT := UTC2TT(Date2JD(eventTime.UTC()))
queryAfterTT := UTC2TT(Date2JD(eventTime.Add(time.Hour).UTC()))
exactNext := NextMoonPlanetConjunction(queryAtTT, planet)
exactClosest := ClosestMoonPlanetConjunction(queryAtTT, planet)
exactLastAfter := LastMoonPlanetConjunction(queryAfterTT, planet)
for name, gotUT := range map[string]float64{
"exactNext": exactNext,
"exactClosest": exactClosest,
"lastAfterEvent": exactLastAfter,
} {
gotTime := JD2DateByZone(gotUT, time.UTC, false)
if diff := math.Abs(gotUT - eventUT); diff > 1e-9 {
t.Fatalf("%s %s mismatch: got %s want %s diff=%v", sample.Planet, name, gotTime.Format(time.RFC3339Nano), eventTime.Format(time.RFC3339Nano), diff*86400)
}
}
}
}
func TestMoonPlanetConjunctionRejectsOppositionBranchJump(t *testing.T) {
query := time.Date(1900, 11, 10, 12, 0, 0, 0, time.UTC)
queryTT := UTC2TT(Date2JD(query))
lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
if math.Abs(lastUT-Date2JD(query)) <= 5.0/86400.0 {
t.Fatalf("last returned query time on branch jump: got %s", JD2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
}
if math.Abs(nextUT-Date2JD(query)) <= 5.0/86400.0 {
t.Fatalf("next returned query time on branch jump: got %s", JD2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano))
}
for name, gotUT := range map[string]float64{
"last": lastUT,
"next": nextUT,
} {
delta := math.Abs(moonPlanetConjunctionDeltaAt(UTC2TT(gotUT), MoonPlanetConjunctionSaturn, -1))
if delta > moonPlanetConjunctionEventTolerance {
t.Fatalf("%s returned non-event candidate: delta=%.8f event=%s", name, delta, JD2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano))
}
}
}
func TestMoonPlanetConjunctionDirectionalOrderingOnSampleQueries(t *testing.T) {
samples := []struct {
planet MoonPlanetConjunctionPlanet
query time.Time
}{
{planet: MoonPlanetConjunctionSaturn, query: time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionMercury, query: time.Date(1900, 1, 14, 12, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionVenus, query: time.Date(1950, 6, 3, 12, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionMars, query: time.Date(2000, 2, 29, 18, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionJupiter, query: time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionSaturn, query: time.Date(2100, 8, 17, 6, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionUranus, query: time.Date(2200, 11, 2, 9, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionNeptune, query: time.Date(2300, 4, 24, 3, 0, 0, 0, time.UTC)},
}
for _, sample := range samples {
queryTT := UTC2TT(Date2JD(sample.query.UTC()))
lastUT := LastMoonPlanetConjunction(queryTT, sample.planet)
nextUT := NextMoonPlanetConjunction(queryTT, sample.planet)
closestUT := ClosestMoonPlanetConjunction(queryTT, sample.planet)
if math.IsNaN(lastUT) || math.IsNaN(nextUT) || math.IsNaN(closestUT) {
t.Fatalf("planet=%v query=%s returned NaN event(s): last=%v next=%v closest=%v", sample.planet, sample.query.Format(time.RFC3339), lastUT, nextUT, closestUT)
}
if !eventUTQueryBeforeOrEqual(lastUT, queryTT) {
t.Fatalf("planet=%v last after query: last=%s query=%s", sample.planet, JD2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano))
}
if !eventUTQueryAfterOrEqual(nextUT, queryTT) {
t.Fatalf("planet=%v next before query: next=%s query=%s", sample.planet, JD2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano))
}
if closestUT != closestEventUTToQueryTT(queryTT, lastUT, nextUT) {
t.Fatalf("planet=%v closest mismatch: got=%s want=%s", sample.planet, JD2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JD2DateByZone(closestEventUTToQueryTT(queryTT, lastUT, nextUT), time.UTC, false).Format(time.RFC3339Nano))
}
for name, gotUT := range map[string]float64{
"last": lastUT,
"next": nextUT,
"closest": closestUT,
} {
delta := math.Abs(moonPlanetConjunctionDeltaAt(UTC2TT(gotUT), sample.planet, -1))
if delta > moonPlanetConjunctionEventTolerance {
t.Fatalf("planet=%v %s returned non-event candidate: delta=%.8f event=%s", sample.planet, name, delta, JD2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano))
}
}
}
}
func TestMoonPlanetConjunctionKeepsImmediateNeighborEvents(t *testing.T) {
query := time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC)
queryTT := UTC2TT(Date2JD(query.UTC()))
lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
closestUT := ClosestMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
wantLast := time.Date(1700, 4, 15, 11, 55, 59, 115569293, time.UTC)
wantNext := time.Date(1700, 5, 13, 0, 35, 5, 981616675, time.UTC)
const tolerance = 5.0 / 86400.0
if diff := math.Abs(lastUT - Date2JD(wantLast)); diff > tolerance {
t.Fatalf("last mismatch: got=%s want=%s diff=%.3fs", JD2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), wantLast.Format(time.RFC3339Nano), diff*86400)
}
if diff := math.Abs(nextUT - Date2JD(wantNext)); diff > tolerance {
t.Fatalf("next mismatch: got=%s want=%s diff=%.3fs", JD2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), wantNext.Format(time.RFC3339Nano), diff*86400)
}
if !sameEventJD(closestUT, lastUT) {
t.Fatalf("closest should keep immediate previous event: closest=%s last=%s", JD2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JD2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
}
}
func TestMoonPlanetConjunctionNextAdvancesPastReturnedEvent(t *testing.T) {
seed := UTC2TT(Date2JD(time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)))
eventUT := NextMoonPlanetConjunction(seed, MoonPlanetConjunctionMercury)
query := JD2DateByZone(eventUT, time.UTC, false).Add(time.Second)
queryTT := UTC2TT(Date2JD(query.UTC()))
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionMercury)
if eventUTQueryTTDelta(nextUT, queryTT) <= 0 {
t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%.6fs",
query.Format(time.RFC3339Nano),
JD2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
eventUTQueryTTDelta(nextUT, queryTT)*86400,
)
}
if sameEventJD(nextUT, eventUT) {
t.Fatalf("next conjunction should advance to a later event: event=%s next=%s",
JD2DateByZone(eventUT, time.UTC, false).Format(time.RFC3339Nano),
JD2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
)
}
}
File diff suppressed because one or more lines are too long

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