Compare commits
24 Commits
126cf68cab
..
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
16c62a97d5
|
|||
|
1f31a9b5b5
|
|||
|
2bf8478639
|
|||
|
9ee2163cc7
|
|||
|
25dc7ac0bc
|
|||
|
a8e7513683
|
|||
|
c8dd777a7b
|
|||
|
46b555cd49
|
|||
|
be3af3884c
|
|||
|
34ff6a36ae
|
|||
|
d40c4dfcd9
|
|||
|
bec7b8a0d8
|
|||
|
3ffdbe0034
|
|||
|
98ff574495
|
|||
|
dadbba7171
|
|||
|
add07bbd85
|
|||
|
27b7e4ab77
|
|||
|
0ab91bcd2d
|
|||
|
543abcafa5
|
|||
|
9f688024e8
|
|||
|
b865e64fa9
|
|||
|
3389c33cb5
|
|||
|
d6b6452304
|
|||
|
438f1700c7
|
@@ -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
@@ -0,0 +1,109 @@
|
||||
# Astro
|
||||
|
||||
**English | [中文](README.md)**
|
||||
|
||||
[](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).
|
||||
@@ -0,0 +1,99 @@
|
||||
# Astro
|
||||
|
||||
**[English](README.en.md) | 中文**
|
||||
|
||||
[](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)。
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package basic
|
||||
|
||||
import "math"
|
||||
|
||||
const rad = math.Pi / 180.0
|
||||
const deg = 180.0 / math.Pi
|
||||
+265
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
@@ -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]
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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("零矢量应判为无效")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user