6 Commits

Author SHA1 Message Date
b612 c8dd777a7b docs: 统一公开 API 的中英双语注释
- 补齐公开接口说明段的英文描述,保持签名注释和详细说明均为中英双语结构
- 规范农历、坐标、公式、轨道、日晷、太阳、恒星及行星事件等 API 的注释口径
2026-05-27 16:08:11 +08:00
b612 46b555cd49 fix: 修复天象事件 API 在事件边界附近的重复返回问题
- 修正合月、合日、留等Last/Next/Closest接口在精确命中和事件后秒级查询时仍返回当前事件的问题,避免应用侧枚举卡死
- 收紧事件边界判定,并补强水星、金星、火星近事件路径的稳定化,保证公开API的单调前进语义
- 补充公开wrapper、跨世纪样本和外部基线回归,覆盖合月邻近事件与边界场景
2026-05-23 23:08:05 +08:00
b612 be3af3884c feat(moon): 新增行星合月查询并修正月球地心赤经赤纬接口
- 修正月球地心真/视赤经赤纬接口口径
- 新增月球与七大行星合月时刻查询
2026-05-23 19:00:53 +08:00
b612 34ff6a36ae fix: 修正行星事件边界与留点计算
- 统一 UT 事件时刻与 TT 查询时刻的边界判断
- 将外行星留点搜索锚定到对应冲日周期
- 修正水星、金星合日、留、大距事件选择
- 统一七大行星视位置计算辅助逻辑
- 增加公开 Last/Next 边界和 JPL/NAOJ 基线回归测试
2026-05-22 12:24:41 +08:00
b612 d40c4dfcd9 fix: 兼容 TinyGo wasm 编译行星星历初始化
- 将 planetViews 从包级初始化改为 sync.Once 懒加载,避免 TinyGo interp 在编译期处理大型 float64 表切片索引时触发 unsupported fcmp
- 将行星视图构建失败改为内部返回 error,并由兼容层统一 panic
- 补充无效行星数据切片边界测试
2026-05-17 21:19:23 +08:00
b612 bec7b8a0d8 feat: 增强日月食搜索、沙罗周期与内行星凌日
- 使用压缩表加速查找日月食沙罗周期信息
- 优化日月食搜索跳步,减少非食季朔望月扫描
- 新增本地日全食、日环食、月全食搜索接口,返回 ok 区分未找到结果
- 新增水星、金星地心凌日查询及测试
2026-05-03 19:00:08 +08:00
101 changed files with 37314 additions and 11390 deletions
+126 -65
View File
@@ -46,7 +46,7 @@ go get b612.me/astro
- Lunar position, rise/set, Earth distance, phase, new/full/quarter times, apparent diameter, bright-limb angle, parallactic angle, geocentric/topocentric libration, apsides, nodes, maximum declination
- `lite/sun` and `lite/moon` lightweight approximation chains for watches, frontends, mini programs, and other resource-constrained environments
- Global and local solar/lunar eclipses, solar central paths, partial footprints, visible local lunar eclipses, Saros metadata, and SVG diagrams
- Seven major planets with positions, rise/set, conjunction/opposition/station events, quadratures, elongations, nodes, phase, apparent magnitude, apparent diameter, parallactic angle, and physical ephemerides
- Seven major planets with positions, rise/set, conjunction/opposition/station events, quadratures, elongations, Mercury/Venus geocentric transits, nodes, phase, apparent magnitude, apparent diameter, parallactic angle, and physical ephemerides
- 9100+ star catalog entries, constellation lookup, proper-motion propagation, rise/set, parallactic angle, and apparent altitude
- Coordinate transforms, topocentric coordinates, sidereal time, precession, nutation, angular distance, refraction, airmass, parallactic angle, and Galactic coordinates
- Standalone formulas for blackbody radiation, synodic periods, photometry, telescope limiting magnitude, stellar radius/temperature/luminosity relations, and airmass models
@@ -63,7 +63,7 @@ go get b612.me/astro
| `moon` | Lunar position, rise/set, phases, new/full/quarter times, apparent altitude, parallactic angle, diameter, bright-limb angle, geocentric/topocentric libration, apsides, nodes, maximum declination |
| `lite/sun` / `lite/moon` | Lightweight Sun/Moon approximation chains for minute-level rise/set, lightweight sky position, and lunar-phase work |
| `eclipse` / `eclipse/svg` | Global/local solar and lunar eclipses, solar central paths, partial footprints, local visibility filtering, Saros metadata, SVG output |
| `mercury` / `venus` | Positions, rise/set, conjunctions, stations, elongations, phase, parallactic angle, magnitude, diameter, nodes, physical ephemerides |
| `mercury` / `venus` | Positions, rise/set, conjunctions, stations, elongations, geocentric transits, phase, parallactic angle, magnitude, diameter, nodes, physical ephemerides |
| `mars` / `jupiter` / `saturn` / `uranus` / `neptune` | Positions, rise/set, conjunction/opposition, stations, quadratures, phase, parallactic angle, magnitude, diameter, nodes, physical ephemerides |
| `earth` | Earth orbital eccentricity, perihelion, aphelion |
| `star` | Constellation lookup, star catalog, proper motion / precession / nutation correction, stellar rise/set, parallactic angle, apparent altitude |
@@ -249,7 +249,6 @@ Output:
```text
[魏明帝 景初三年腊月二十 蜀后主 延熙二年冬月十九 吴大帝 赤乌二年冬月二十] // one Gregorian instant maps to parallel Three Kingdoms lunisolar results
[
{
"solarDate": "0240-01-01T08:08:08.000000008+08:00",
@@ -309,7 +308,6 @@ Output:
"chineseZodiac": "羊"
}
] // structured lunisolar records, one object per matching historical result
1083-11-24 00:00:00 +0800 CST // Gregorian date corresponding to 元丰六年十月十二日
[宋神宗 元丰六年十月十二 辽道宗 大康九年十月十二] // the same day also matches a Liao calendar result
2026-02-17 00:00:00 +0800 CST // Chinese New Year in 2026
@@ -341,10 +339,10 @@ func main() {
Output:
```text
2020-02-04 17:03:17.820854187 +0800 CST // Beginning of Spring
2020-12-21 18:02:17.568823993 +0800 CST // Winter Solstice
2020-03-20 11:49:34.502393603 +0800 CST // March Equinox
2020-03-20 11:49:34.502393603 +0800 CST // same result from direct longitude input
2020-02-04 17:03:20.471614301 +0800 CST // Beginning of Spring
2020-12-21 18:02:20.648710727 +0800 CST // Winter Solstice
2020-03-20 11:49:37.149532735 +0800 CST // March Equinox
2020-03-20 11:49:37.149532735 +0800 CST // same result from direct longitude input
```
### Sun And Moon
@@ -400,14 +398,14 @@ func main() {
Output:
```text
2020-01-01 07:22:27.964431345 +0800 CST <nil> // civil morning twilight begins
2020-01-01 07:50:14.534510672 +0800 CST <nil> // sunrise
2020-01-01 07:22:27.960488498 +0800 CST <nil> // civil morning twilight begins
2020-01-01 07:50:14.530648291 +0800 CST <nil> // sunrise
2020-01-01 12:47:35.933117866 +0800 CST // solar upper culmination
2020-01-01 17:44:47.076647579 +0800 CST <nil> // sunset
2020-01-01 18:12:33.629668056 +0800 CST <nil> // civil evening twilight ends
2020-01-01 11:52:44.643359184 +0800 CST <nil> // moonrise
2020-01-01 17:38:03.879639208 +0800 CST // lunar upper culmination
2020-01-01 23:26:52.202896177 +0800 CST <nil> // moonset
2020-01-01 17:44:47.070974707 +0800 CST <nil> // sunset
2020-01-01 18:12:33.624035418 +0800 CST <nil> // civil evening twilight ends
2020-01-01 11:52:45.157297253 +0800 CST <nil> // moonrise
2020-01-01 17:38:02.510787248 +0800 CST // lunar upper culmination
2020-01-01 23:26:51.580328643 +0800 CST <nil> // moonset
```
#### Sun and Moon position
@@ -461,17 +459,16 @@ func main() {
Output:
```text
280.0152925179703 // apparent ecliptic longitude of the Sun, degrees
23.436215552851408 // true obliquity of the ecliptic, degrees
RA: 18h43m34.83s Dec: -23°330.25″ // apparent RA and Dec of the Sun
280.01526210031136 // apparent ecliptic longitude of the Sun, degrees
23.4362178391013 // true obliquity of the ecliptic, degrees
RA: 18h43m34.82s Dec: -23°330.27″ // apparent RA and Dec of the Sun
Sagittarius // English constellation containing the Sun
Azimuth: 120.19483856399326 Altitude: 2.4014324584398516 Zenith: 87.59856754156014 // solar horizontal coordinates at Xi'an
0.9832929365443133 // Sun-Earth distance, AU
RA: 23h17m51.93s Dec: -10°1917.02″ // topocentric apparent RA and Dec of the Moon
Azimuth: 120.19477090015224 Altitude: 2.4014437419430097 Zenith: 87.59855625805699 // solar horizontal coordinates at Xi'an
0.983292937163176 // Sun-Earth distance, AU
RA: 23h17m53.15s Dec: -10°1918.57″ // topocentric apparent RA and Dec of the Moon
Aquarius // English constellation containing the Moon
Azimuth: 67.84449893794012 Altitude: -45.13018696439911 Zenith: 135.13018696439912 // lunar horizontal coordinates at Xi'an
404238.6354387698 // Earth-Moon distance, km
Azimuth: 67.84050700509859 Altitude: -45.13425530765482 Zenith: 135.13425530765483 // lunar horizontal coordinates at Xi'an
404238.6096080479 // Earth-Moon distance, km
```
`sun.Physical` / `sun.PhysicalN` return:
@@ -609,12 +606,12 @@ func main() {
Output:
```text
0.3000437415436273 // about 30% of the lunar disk is illuminated
0.300041309608744 // about 30% of the lunar disk is illuminated
上峨眉月 // Chinese phase description
2020-01-25 05:41:55.820311009 +0800 CST // next new moon
2020-01-03 12:45:20.809730887 +0800 CST // next first quarter
2020-01-11 03:21:14.729664623 +0800 CST // next full moon
2020-01-17 20:58:20.955985486 +0800 CST // next last quarter
2020-01-25 05:41:58.271192908 +0800 CST // next new moon
2020-01-03 12:45:23.229190707 +0800 CST // next first quarter
2020-01-11 03:21:17.159625291 +0800 CST // next full moon
2020-01-17 20:58:23.396406769 +0800 CST // next last quarter
```
Phase aliases:
@@ -674,6 +671,8 @@ Common entry points:
- `LastSolarEclipse` / `NextSolarEclipse` / `ClosestSolarEclipse`: search global solar eclipses
- `LocalSolarEclipseOnDate`: detect whether a site can see a local solar eclipse on that date
- `LastLocalSolarEclipse` / `NextLocalSolarEclipse` / `ClosestLocalSolarEclipse`: search locally visible solar eclipses
- `LastLocalTotalSolarEclipse` / `NextLocalTotalSolarEclipse` / `ClosestLocalTotalSolarEclipse`: search locally visible total solar eclipses, returning `(info, ok)`
- `LastLocalAnnularSolarEclipse` / `NextLocalAnnularSolarEclipse` / `ClosestLocalAnnularSolarEclipse`: search locally visible annular solar eclipses, returning `(info, ok)`
- `SolarEclipseCentralPath`: compute central line, northern/southern limits, and greatest-eclipse point
- `SolarEclipsePartialFootprints`: compute the partial-eclipse penumbral footprint on Earth
- `eclipse/svg.LocalSolarEclipseSVG`: render a local solar-disk SVG
@@ -925,6 +924,7 @@ Common entry points:
- `LastLunarEclipse` / `NextLunarEclipse` / `ClosestLunarEclipse`: search global lunar eclipses
- `LocalLunarEclipseOnDate`: detect whether a visible lunar eclipse is visible from a site on a local date
- `LastLocalLunarEclipse` / `NextLocalLunarEclipse` / `ClosestLocalLunarEclipse`: search visible local lunar eclipses
- `LastLocalTotalLunarEclipse` / `NextLocalTotalLunarEclipse` / `ClosestLocalTotalLunarEclipse`: search visible local total lunar eclipses, returning `(info, ok)`
- `GeometricLocalLunarEclipseOnDate`: detect geometric lunar eclipse overlap without filtering by whether the Moon is above the horizon
- `eclipse/svg.LunarEclipseSVG`: render a lunar-eclipse shadow-path SVG
@@ -1002,18 +1002,18 @@ Output:
```text
total // eclipse type
true {125 49 72} // Lunar Saros 125, member 49/72
2028-12-31 16:52:05.257715537 +0000 UTC // greatest eclipse
2.273989043 1.246114288 // penumbral and umbral magnitudes
2028-12-31 14:03:54.163612125 +0000 UTC // P1
2028-12-31 15:07:42.293254197 +0000 UTC // U1
2028-12-31 16:16:27.717077732 +0000 UTC // U2
2028-12-31 17:27:46.687390804 +0000 UTC // U3
2028-12-31 18:36:32.272528112 +0000 UTC // U4
2028-12-31 19:40:11.173523784 +0000 UTC // P4
2.29960334 1.25117109 // Chauvenet penumbral and umbral magnitudes
2028-12-31 16:52:05.566135346 +0000 UTC // greatest eclipse
2.273989043382249 1.2461142882946992 // penumbral and umbral magnitudes
2028-12-31 14:03:54.219463169 +0000 UTC // P1
2028-12-31 15:07:42.115980684 +0000 UTC // U1
2028-12-31 16:16:27.24464178 +0000 UTC // U2
2028-12-31 17:27:46.214954853 +0000 UTC // U3
2028-12-31 18:36:32.251235246 +0000 UTC // U4
2028-12-31 19:40:11.52023971 +0000 UTC // P4
2.2996033397593934 1.2511710895700923 // Chauvenet penumbral and umbral magnitudes
true // local date overlaps an eclipse
total // local eclipse type
2029-01-01 00:52:05.257715537 +0800 CST // greatest eclipse in UTC+8
2029-01-01 00:52:05.566135346 +0800 CST // greatest eclipse in UTC+8
```
#### Checks against NASA data
@@ -1153,20 +1153,20 @@ func main() {
Output:
```text
2019-11-11 23:21:39.702344834 +0800 CST // previous inferior conjunction of Mercury
2021-03-26 14:57:38.289429545 +0800 CST // next superior conjunction of Venus
2019-11-01 04:31:47.807287573 +0800 CST // previous Mercury station from prograde to retrograde
2021-12-18 18:59:12.762369811 +0800 CST // next Venus station from retrograde to prograde
2019-10-20 11:59:33.893027007 +0800 CST // previous greatest eastern elongation of Mercury
2020-08-13 07:56:02.326616048 +0800 CST // next greatest western elongation of Venus
2020-01-01 10:01:10.821288228 +0800 CST <nil> // Venus rise time in Xi'an; no error
2020-01-01 20:27:00.741534233 +0800 CST <nil> // Venus set time in Xi'an; no error
2019-11-11 23:21:42.048057317 +0800 CST // previous inferior conjunction of Mercury
2021-03-26 14:57:43.01215589 +0800 CST // next superior conjunction of Venus
2019-11-01 04:31:38.999851942 +0800 CST // previous Mercury station from prograde to retrograde
2020-06-25 02:07:41.549940705 +0800 CST // next Venus station from retrograde to prograde
2019-10-20 11:50:28.734245896 +0800 CST // previous greatest eastern elongation of Mercury
2020-08-13 07:59:17.123789191 +0800 CST // next greatest western elongation of Venus
2020-01-01 10:02:34.172194004 +0800 CST <nil> // Venus rise time in Xi'an; no error
2020-01-01 20:25:37.363712489 +0800 CST <nil> // Venus set time in Xi'an; no error
-4 // Venus apparent magnitude
49.98145049145023 // Venus phase angle, degrees
0.8215177914415865 // illuminated fraction of Venus
255.63802111818407 // bright-limb position angle of Venus, degrees
1.2760033106813273 // Earth-Venus distance, AU
0.7262288470390035 // Sun-Venus distance, AU
255.63802093000768 // bright-limb position angle of Venus, degrees
1.2778819631550336 // Earth-Venus distance, AU
0.7262651056423838 // Sun-Venus distance, AU
```
Inner and outer planets also expose `Diameter` / `Semidiameter` and `N` variants, returning geocentric apparent diameter/semidiameter in arcseconds.
@@ -1191,6 +1191,67 @@ For `date := 2020-01-01 08:08:08 CST`, the output is:
76.86008484515058 256.8600848451506 // Venus ascending-node and descending-node longitudes, degrees
```
Mercury and Venus also expose `NextTransit` / `LastTransit` / `ClosestTransit` for geocentric planetary transits. "Geocentric" means the planet disk crosses the solar disk as seen from Earth's center; it does not test whether the Sun is above the horizon at a particular observing site. For observing plans, combine this with local solar altitude and weather.
```go
package main
import (
"fmt"
"time"
"b612.me/astro/mercury"
"b612.me/astro/venus"
)
func main() {
// Next geocentric Mercury transit after the beginning of 2019.
mercuryTransit := mercury.NextTransit(time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC))
fmt.Println(mercuryTransit.Valid)
fmt.Println(mercuryTransit.Start)
fmt.Println(mercuryTransit.InternalStart)
fmt.Println(mercuryTransit.Greatest)
fmt.Println(mercuryTransit.InternalEnd)
fmt.Println(mercuryTransit.End)
fmt.Println(mercuryTransit.Duration)
fmt.Println(mercuryTransit.MinimumSeparationArcsec)
fmt.Println(mercuryTransit.SunSemidiameterArcsec)
fmt.Println(mercuryTransit.PlanetSemidiameterArcsec)
// Next geocentric Venus transit after the beginning of 2012.
venusTransit := venus.NextTransit(time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC))
fmt.Println(venusTransit.Valid)
fmt.Println(venusTransit.Start)
fmt.Println(venusTransit.InternalStart)
fmt.Println(venusTransit.Greatest)
fmt.Println(venusTransit.InternalEnd)
fmt.Println(venusTransit.End)
fmt.Println(venusTransit.Duration)
}
```
Output:
```text
true // a valid geocentric Mercury transit was found
2019-11-11 12:35:31.617325544 +0000 UTC // first contact: Mercury externally enters the solar disk
2019-11-11 12:37:13.078211545 +0000 UTC // second contact: Mercury is fully inside the solar disk
2019-11-11 15:19:48.410291075 +0000 UTC // greatest transit: Mercury center is closest to the Sun center
2019-11-11 18:02:29.2267102 +0000 UTC // third contact: Mercury starts leaving the solar disk
2019-11-11 18:04:10.687676668 +0000 UTC // fourth contact: Mercury externally leaves the solar disk
5h28m39.070351124s // geocentric transit duration from first to fourth contact
75.92506897631685 // minimum Mercury-Sun center separation at greatest transit, arcseconds
968.8881520858397 // solar semidiameter at greatest transit, arcseconds
4.978442860728242 // Mercury semidiameter at greatest transit, arcseconds
true // a valid geocentric Venus transit was found
2012-06-05 22:09:47.514281272 +0000 UTC // first contact: Venus externally enters the solar disk
2012-06-05 22:27:35.701768398 +0000 UTC // second contact: Venus is fully inside the solar disk
2012-06-06 01:29:35.408823788 +0000 UTC // greatest transit: Venus center is closest to the Sun center
2012-06-06 04:31:34.90493685 +0000 UTC // third contact: Venus starts leaving the solar disk
2012-06-06 04:49:23.303366303 +0000 UTC // fourth contact: Venus externally leaves the solar disk
6h39m35.789085031s // geocentric transit duration from first to fourth contact
```
#### Outer planets
```go
@@ -1251,18 +1312,18 @@ func main() {
Output:
```text
2020-10-14 07:25:47.740884125 +0800 CST // next opposition of Mars
2021-01-29 09:39:30.916356146 +0800 CST // next conjunction of Jupiter
2019-04-30 10:28:27.453395426 +0800 CST // previous Saturn station from prograde to retrograde
saturn B=23.577026 Bp=23.266930 P=6.629811 dU=1.171017 major=34.133852 minor=13.652911 // Saturn ring B, B', P, dU, major axis, minor axis
2021-01-14 21:35:01.269377768 +0800 CST // next Uranus station from retrograde to prograde
2019-12-08 17:00:13.772284984 +0800 CST // previous eastern quadrature of Neptune
2020-06-07 03:10:57.179121673 +0800 CST // next western quadrature of Mars
2020-01-01 04:40:05.409269034 +0800 CST <nil> // Mars rise time in Xi'an; no error
2020-01-01 14:56:57.175483703 +0800 CST <nil> // Mars set time in Xi'an; no error
2020-10-14 07:25:50.262777507 +0800 CST // next opposition of Mars
2021-01-29 09:39:33.565426468 +0800 CST // next conjunction of Jupiter
2019-04-30 10:27:41.606289446 +0800 CST // previous Saturn station from prograde to retrograde
saturn B=23.577026 Bp=23.266930 P=6.629811 dU=1.171016 major=34.133852 minor=13.652911 // Saturn ring B, B', P, dU, major axis, minor axis
2020-01-11 15:23:07.378419935 +0800 CST // next Uranus station from retrograde to prograde
2019-12-08 17:00:15.328663587 +0800 CST // previous eastern quadrature of Neptune
2020-06-07 03:10:59.356176853 +0800 CST // next western quadrature of Mars
2020-01-01 04:41:29.622089266 +0800 CST <nil> // Mars rise time in Xi'an; no error
2020-01-01 14:55:32.963870465 +0800 CST <nil> // Mars set time in Xi'an; no error
1.57 // Mars apparent magnitude
2.1820316323604088 // Earth-Mars distance, AU
1.5894169865107062 // Sun-Mars distance, AU
2.1844284956325937 // Earth-Mars distance, AU
1.5897860004265403 // Sun-Mars distance, AU
```
`saturn.Ring` returns `RingInfo`: `EarthLatitude` is ring opening angle B, `SunLatitude` is B', `PositionAngle` is the position angle of the northern semiminor axis, `DeltaU` is the Saturnicentric longitude difference between the Sun and Earth in the ring plane, and `MajorAxis` / `MinorAxis` are the apparent outer major/minor axes in arcseconds.
@@ -1312,7 +1373,7 @@ Output:
```text
jupiter DS=54.342153 DE=1.436485 CMI=292.712909 CMII=276.309048 CMIII=147.241811 // Jupiter DS/DE and System I/II/III central meridians, degrees
saturn B=-0.608048 Bp=-2.675677 P=4.480276 major=42.709920 minor=0.453248 // Saturn ring B, B', minor-axis position angle, outer major/minor axes
saturn B=-0.608046 Bp=-2.675677 P=4.480276 major=42.709920 minor=0.453246 // Saturn ring B, B', minor-axis position angle, outer major/minor axes
```
If only Jupiter central meridians are needed:
@@ -1475,8 +1536,8 @@ Output:
2019-12-31 19:22:56.176710426 +0800 CST // rise time of Sirius
2020-01-01 05:30:39.834894239 +0800 CST // set time of Sirius
Canis Major // English constellation containing Sirius
5h58m10.19s // right ascension of Vega in year 13600
84°1926.25″ // declination of Vega in year 13600
5h58m5.71s // right ascension of Vega in year 13600
84°1926.13″ // declination of Vega in year 13600
天狼 Sirius -1.46 // first brightest-star entry: Chinese name, common English name, apparent magnitude
```
@@ -1767,7 +1828,7 @@ Notes:
- `lite/sun` and `lite/moon` lightweight Sun/Moon chains for minute-level rise/set, lightweight position, and lunar-phase work
- Earth eccentricity, Sun-Earth distance, perihelion, aphelion
- Apparent/mean sidereal time, constellation lookup, common coordinate transforms, refraction, airmass, parallactic angle, Galactic coordinates
- Seven major-planet coordinates, Sun/body and Earth/body distances, special events, physical ephemerides, apparent diameters, phases, parallactic angles, and nodes
- Seven major-planet coordinates, Sun/body and Earth/body distances, special events, Mercury/Venus geocentric transits, physical ephemerides, apparent diameters, phases, parallactic angles, and nodes
- Chinese lunisolar calendar conversion from 104 BCE to 3000 CE
- 9100+ star catalog
- Generic small-body orbit propagation, H-G apparent magnitude, visual-binary position angle and separation
+127 -67
View File
@@ -44,7 +44,7 @@ go get b612.me/astro
- 🌙 **月亮计算**:天球位置、月出月落、地月距离、月相、朔望时间、视直径、亮边位置角、视差角、地心/站心天平动、近远地点、交点、最大赤纬等
- 🪶 **轻量链路**`lite/sun``lite/moon` 提供面向手表、前端、小程序和其它资源受限环境的轻量近似太阳/月亮算法,覆盖天球位置、升落和月相
- 🌗 **日月食**:全局日食、站心日食、中心线/偏食足迹、月食、地方可见月食与 SVG 示意图
- 🪐 **行星计算**:七大行星天球位置、升落时间、合冲留等特殊天象时间、升交点/降交点、视直径/视半径、相位、视差角、节点、视星等与物理星历
- 🪐 **行星计算**:七大行星天球位置、升落时间、合冲留、大距、水星/金星地心凌日等特殊天象时间、升交点/降交点、视直径/视半径、相位、视差角、节点、视星等与物理星历
-**恒星计算**:指定天球坐标所属星座;同时包含9100颗恒星数据库,可计算升降时间、视差角和视高度角,获取指定日期的恒星坐标信息
- 🧭 **坐标工具**:黄道/赤道/地平坐标转换、站心坐标、恒星时、岁差、章动、角距离、大气折射、大气质量、视差角、银道坐标
- 🔭 **研究公式**:黑体辐射、会合周期、星等距离换算、望远镜极限星等、恒星半径/温度/光度换算、大气质量模型
@@ -61,7 +61,7 @@ go get b612.me/astro
| `moon` | 月亮位置、月出月落、月相、朔望弦、视高度角、视差角、视直径、亮边位置角、地心/站心天平动、近远地点、交点、最大赤纬 |
| `lite/sun` / `lite/moon` | 轻量太阳/月亮近似链路,面向分钟级升落、轻量天球位置和月相计算 |
| `eclipse` / `eclipse/svg` | 全局/局地日月食、日食中心线与偏食足迹、局地可见性筛选、日月食 SVG |
| `mercury` / `venus` | 水星、金星位置、升落、合日、留、大距、相位、视差角、视星等、视直径、节点和物理星历 |
| `mercury` / `venus` | 水星、金星位置、升落、合日、留、大距、地心凌日、相位、视差角、视星等、视直径、节点和物理星历 |
| `mars` / `jupiter` / `saturn` / `uranus` / `neptune` | 外行星位置、升落、合冲、留、方照、相位、视差角、视星等、视直径、节点和物理星历 |
| `earth` | 地球轨道偏心率、近日点、远日点 |
| `star` | 星座判定、恒星数据库、恒星自行/岁差/章动修正、恒星升落、视差角、视高度角 |
@@ -309,7 +309,6 @@ func main() {
```text
// 同一公历时刻在三国并立时期会映射到多个政权各自的农历结果
[魏明帝 景初三年腊月二十 蜀后主 延熙二年冬月十九 吴大帝 赤乌二年冬月二十]
// 结构化农历信息输出;每个对象对应一个政权口径下的结果
[
{
@@ -370,14 +369,12 @@ func main() {
"chineseZodiac": "羊"
}
]
// “元丰六年十月十二日”对应的公历日期
1083-11-24 00:00:00 +0800 CST
// 同一天在并行政权下还会命中辽道宗大康九年十月十二
[宋神宗 元丰六年十月十二 辽道宗 大康九年十月十二]
// 现代农历日期转换结果;2026 年正月初一对应 2026-02-17
2026-02-17 00:00:00 +0800 CST //2026年春节
2026-02-17 00:00:00 +0800 CST
```
#### 节气
@@ -407,10 +404,10 @@ func main() {
输出结果
```
2020-02-04 17:03:17.820854187 +0800 CST
2020-12-21 18:02:17.568823993 +0800 CST
2020-03-20 11:49:34.502393603 +0800 CST
2020-03-20 11:49:34.502393603 +0800 CST
2020-02-04 17:03:20.471614301 +0800 CST
2020-12-21 18:02:20.648710727 +0800 CST
2020-03-20 11:49:37.149532735 +0800 CST
2020-03-20 11:49:37.149532735 +0800 CST
```
@@ -474,14 +471,14 @@ func main() {
输出结果
```
2020-01-01 07:22:27.964431345 +0800 CST <nil>
2020-01-01 07:50:14.534510672 +0800 CST <nil>
2020-01-01 07:22:27.960488498 +0800 CST <nil>
2020-01-01 07:50:14.530648291 +0800 CST <nil>
2020-01-01 12:47:35.933117866 +0800 CST
2020-01-01 17:44:47.076647579 +0800 CST <nil>
2020-01-01 18:12:33.629668056 +0800 CST <nil>
2020-01-01 11:52:44.643359184 +0800 CST <nil>
2020-01-01 17:38:03.879639208 +0800 CST
2020-01-01 23:26:52.202896177 +0800 CST <nil>
2020-01-01 17:44:47.070974707 +0800 CST <nil>
2020-01-01 18:12:33.624035418 +0800 CST <nil>
2020-01-01 11:52:45.157297253 +0800 CST <nil>
2020-01-01 17:38:02.510787248 +0800 CST
2020-01-01 23:26:51.580328643 +0800 CST <nil>
```
@@ -536,17 +533,16 @@ func main() {
输出结果:
```
280.0152925179703
23.436215552851408
赤经: 18h43m34.83s 赤纬: -23°330.25
280.01526210031136
23.4362178391013
赤经: 18h43m34.82s 赤纬: -23°330.27
人马座
方位角: 120.19483856399326 高度角: 2.4014324584398516 天顶距: 87.59856754156014
0.9832929365443133
赤经: 23h17m51.93s 赤纬: -10°1917.02″
方位角: 120.19477090015224 高度角: 2.4014437419430097 天顶距: 87.59855625805699
0.983292937163176
赤经: 23h17m53.15s 赤纬: -10°1918.57″
宝瓶座
方位角: 67.84449893794012 高度角: -45.13018696439911 天顶距: 135.13018696439912
404238.6354387698
方位角: 67.84050700509859 高度角: -45.13425530765482 天顶距: 135.13425530765483
404238.6096080479
```
太阳还提供 `sun.Physical` / `sun.PhysicalN`,返回:
@@ -683,12 +679,12 @@ func main() {
输出结果:
```
0.3000437415436273 // 月面约有 30% 被太阳照亮
0.300041309608744 // 月面约有 30% 被太阳照亮
上峨眉月 // 当前月相描述
2020-01-25 05:41:55.820311009 +0800 CST // 下一次朔月
2020-01-03 12:45:20.809730887 +0800 CST // 下一次上弦
2020-01-11 03:21:14.729664623 +0800 CST // 下一次望月,也就是满月
2020-01-17 20:58:20.955985486 +0800 CST // 下一次下弦
2020-01-25 05:41:58.271192908 +0800 CST // 下一次朔月
2020-01-03 12:45:23.229190707 +0800 CST // 下一次上弦
2020-01-11 03:21:17.159625291 +0800 CST // 下一次望月,也就是满月
2020-01-17 20:58:23.396406769 +0800 CST // 下一次下弦
```
月相四个相位同时提供拼音名和英文 alias,例如:
@@ -742,6 +738,8 @@ func main() {
- `LastSolarEclipse` / `NextSolarEclipse` / `ClosestSolarEclipse`:搜索全局日食
- `LocalSolarEclipseOnDate`:判断某地当天是否能看到站心日食
- `LastLocalSolarEclipse` / `NextLocalSolarEclipse` / `ClosestLocalSolarEclipse`:搜索某地可见的站心日食
- `LastLocalTotalSolarEclipse` / `NextLocalTotalSolarEclipse` / `ClosestLocalTotalSolarEclipse`:搜索某地可见的日全食,返回 `(info, ok)`
- `LastLocalAnnularSolarEclipse` / `NextLocalAnnularSolarEclipse` / `ClosestLocalAnnularSolarEclipse`:搜索某地可见的日环食,返回 `(info, ok)`
- `SolarEclipseCentralPath`:计算中心线、南北界和食甚点
- `SolarEclipsePartialFootprints`:计算偏食半影在地球表面的足迹
- `eclipse/svg.LocalSolarEclipseSVG`:生成某地的日面视圆 SVG
@@ -995,6 +993,7 @@ true 13424 // 北京日全食 SVG 生成成功,长度 13424 字节
- `LastLunarEclipse` / `NextLunarEclipse` / `ClosestLunarEclipse`:搜索全局月食
- `LocalLunarEclipseOnDate`:判断某地当天是否能看到可见月食
- `LastLocalLunarEclipse` / `NextLocalLunarEclipse` / `ClosestLocalLunarEclipse`:搜索某地可见月食
- `LastLocalTotalLunarEclipse` / `NextLocalTotalLunarEclipse` / `ClosestLocalTotalLunarEclipse`:搜索某地可见月全食,返回 `(info, ok)`
- `GeometricLocalLunarEclipseOnDate`:判断某地当天是否发生几何月食,不做“月亮在地平线上方”的可见性过滤
- `eclipse/svg.LunarEclipseSVG`:生成月食穿影图 SVG
@@ -1071,18 +1070,18 @@ func main() {
```text
total
true {125 49 72}
2028-12-31 16:52:05.257715537 +0000 UTC
2.273989043 1.246114288
2028-12-31 14:03:54.163612125 +0000 UTC
2028-12-31 15:07:42.293254197 +0000 UTC
2028-12-31 16:16:27.717077732 +0000 UTC
2028-12-31 17:27:46.687390804 +0000 UTC
2028-12-31 18:36:32.272528112 +0000 UTC
2028-12-31 19:40:11.173523784 +0000 UTC
2.29960334 1.25117109
2028-12-31 16:52:05.566135346 +0000 UTC
2.273989043382249 1.2461142882946992
2028-12-31 14:03:54.219463169 +0000 UTC
2028-12-31 15:07:42.115980684 +0000 UTC
2028-12-31 16:16:27.24464178 +0000 UTC
2028-12-31 17:27:46.214954853 +0000 UTC
2028-12-31 18:36:32.251235246 +0000 UTC
2028-12-31 19:40:11.52023971 +0000 UTC
2.2996033397593934 1.2511710895700923
true
total
2029-01-01 00:52:05.257715537 +0800 CST
2029-01-01 00:52:05.566135346 +0800 CST
```
##### 与 NASA 数据对照
@@ -1229,20 +1228,20 @@ func main() {
输出结果:
```
2019-11-11 23:21:39.702344834 +0800 CST // 水星上次下合
2021-03-26 14:57:38.289429545 +0800 CST // 金星下次上合
2019-11-01 04:31:47.807287573 +0800 CST // 水星上次由顺行转逆行的留
2021-12-18 18:59:12.762369811 +0800 CST // 金星下次由逆行转顺行的留
2019-10-20 11:59:33.893027007 +0800 CST // 水星上次东大距
2020-08-13 07:56:02.326616048 +0800 CST // 金星下次西大距
2020-01-01 10:01:10.821288228 +0800 CST <nil> // 西安当天金星升起时刻;无错误
2020-01-01 20:27:00.741534233 +0800 CST <nil> // 西安当天金星落下时刻;无错误
2019-11-11 23:21:42.048057317 +0800 CST // 水星上次下合
2021-03-26 14:57:43.01215589 +0800 CST // 金星下次上合
2019-11-01 04:31:38.999851942 +0800 CST // 水星上次由顺行转逆行的留
2020-06-25 02:07:41.549940705 +0800 CST // 金星下次由逆行转顺行的留
2019-10-20 11:50:28.734245896 +0800 CST // 水星上次东大距
2020-08-13 07:59:17.123789191 +0800 CST // 金星下次西大距
2020-01-01 10:02:34.172194004 +0800 CST <nil> // 西安当天金星升起时刻;无错误
2020-01-01 20:25:37.363712489 +0800 CST <nil> // 西安当天金星落下时刻;无错误
-4 // 金星视星等
49.98145049145023 // 金星相位角,单位度
0.8215177914415865 // 金星被照亮比例
255.63802111818407 // 金星亮面中心位置角,单位度
1.2760033106813273 // 金地距离,单位 AU
0.7262288470390035 // 金日距离,单位 AU
255.63802093000768 // 金星亮面中心位置角,单位度
1.2778819631550336 // 金地距离,单位 AU
0.7262651056423838 // 金日距离,单位 AU
```
内外行星同样提供 `Diameter` / `Semidiameter`(以及 `N` 版),返回地心视直径/视半径,单位为角秒。
@@ -1267,6 +1266,67 @@ fmt.Println(venus.AscendingNode(date), venus.DescendingNode(date))
76.86008484515058 256.8600848451506 // 金星升交点、降交点黄经,单位度
```
水星和金星还提供 `NextTransit` / `LastTransit` / `ClosestTransit` 地心凌日查询。这里的“地心”指从地球中心看到的行星圆面经过太阳圆面,不判断某个地点当时太阳是否在地平线上;如果要做观测计划,还需要结合本地太阳高度角和天气条件。
```go
package main
import (
"fmt"
"time"
"b612.me/astro/mercury"
"b612.me/astro/venus"
)
func main() {
// 查询 2019 年之后下一次地心水星凌日。
mercuryTransit := mercury.NextTransit(time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC))
fmt.Println(mercuryTransit.Valid)
fmt.Println(mercuryTransit.Start)
fmt.Println(mercuryTransit.InternalStart)
fmt.Println(mercuryTransit.Greatest)
fmt.Println(mercuryTransit.InternalEnd)
fmt.Println(mercuryTransit.End)
fmt.Println(mercuryTransit.Duration)
fmt.Println(mercuryTransit.MinimumSeparationArcsec)
fmt.Println(mercuryTransit.SunSemidiameterArcsec)
fmt.Println(mercuryTransit.PlanetSemidiameterArcsec)
// 查询 2012 年之后下一次地心金星凌日。
venusTransit := venus.NextTransit(time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC))
fmt.Println(venusTransit.Valid)
fmt.Println(venusTransit.Start)
fmt.Println(venusTransit.InternalStart)
fmt.Println(venusTransit.Greatest)
fmt.Println(venusTransit.InternalEnd)
fmt.Println(venusTransit.End)
fmt.Println(venusTransit.Duration)
}
```
输出结果:
```text
true // 找到一次有效的地心水星凌日
2019-11-11 12:35:31.617325544 +0000 UTC // 一触:水星外切进入太阳圆面
2019-11-11 12:37:13.078211545 +0000 UTC // 二触:水星完全进入太阳圆面
2019-11-11 15:19:48.410291075 +0000 UTC // 凌甚:水星中心最接近太阳中心
2019-11-11 18:02:29.2267102 +0000 UTC // 三触:水星开始离开太阳圆面
2019-11-11 18:04:10.687676668 +0000 UTC // 四触:水星外切离开太阳圆面
5h28m39.070351124s // 一触到四触的地心凌日持续时间
75.92506897631685 // 凌甚时水星中心与太阳中心的最小角距离,单位角秒
968.8881520858397 // 凌甚时太阳视半径,单位角秒
4.978442860728242 // 凌甚时水星视半径,单位角秒
true // 找到一次有效的地心金星凌日
2012-06-05 22:09:47.514281272 +0000 UTC // 一触:金星外切进入太阳圆面
2012-06-05 22:27:35.701768398 +0000 UTC // 二触:金星完全进入太阳圆面
2012-06-06 01:29:35.408823788 +0000 UTC // 凌甚:金星中心最接近太阳中心
2012-06-06 04:31:34.90493685 +0000 UTC // 三触:金星开始离开太阳圆面
2012-06-06 04:49:23.303366303 +0000 UTC // 四触:金星外切离开太阳圆面
6h39m35.789085031s // 一触到四触的地心凌日持续时间
```
#### 外行星
```go
@@ -1326,18 +1386,18 @@ func main() {
输出结果:
```
2020-10-14 07:25:47.740884125 +0800 CST // 火星下次冲日
2021-01-29 09:39:30.916356146 +0800 CST // 木星下次合日
2019-04-30 10:28:27.453395426 +0800 CST // 土星上次由顺行转逆行的留
saturn B=23.577026 Bp=23.266930 P=6.629811 dU=1.171017 major=34.133852 minor=13.652911 // 土星环 B、B'、P、dU、长轴、短轴
2021-01-14 21:35:01.269377768 +0800 CST // 天王星下次由逆行转顺行的留
2019-12-08 17:00:13.772284984 +0800 CST // 海王星上次东方照
2020-06-07 03:10:57.179121673 +0800 CST // 火星下次西方照
2020-01-01 04:40:05.409269034 +0800 CST <nil> // 西安当天火星升起时刻;无错误
2020-01-01 14:56:57.175483703 +0800 CST <nil> // 西安当天火星落下时刻;无错误
2020-10-14 07:25:50.262777507 +0800 CST // 火星下次冲日
2021-01-29 09:39:33.565426468 +0800 CST // 木星下次合日
2019-04-30 10:27:41.606289446 +0800 CST // 土星上次由顺行转逆行的留
saturn B=23.577026 Bp=23.266930 P=6.629811 dU=1.171016 major=34.133852 minor=13.652911 // 土星环 B、B'、P、dU、长轴、短轴
2020-01-11 15:23:07.378419935 +0800 CST // 天王星下次由逆行转顺行的留
2019-12-08 17:00:15.328663587 +0800 CST // 海王星上次东方照
2020-06-07 03:10:59.356176853 +0800 CST // 火星下次西方照
2020-01-01 04:41:29.622089266 +0800 CST <nil> // 西安当天火星升起时刻;无错误
2020-01-01 14:55:32.963870465 +0800 CST <nil> // 西安当天火星落下时刻;无错误
1.57 // 火星视星等
2.1820316323604088 // 地火距离,单位 AU
1.5894169865107062 // 日火距离,单位 AU
2.1844284956325937 // 地火距离,单位 AU
1.5897860004265403 // 日火距离,单位 AU
```
@@ -1388,7 +1448,7 @@ func main() {
```text
jupiter DS=54.342153 DE=1.436485 CMI=292.712909 CMII=276.309048 CMIII=147.241811 // 木星子日/子地赤纬,System I/II/III 中央经线,单位度
saturn B=-0.608048 Bp=-2.675677 P=4.480276 major=42.709920 minor=0.453248 // 土星环 B、B'、短轴位置角、外缘长短轴,角度单位度,长短轴单位角秒
saturn B=-0.608046 Bp=-2.675677 P=4.480276 major=42.709920 minor=0.453246 // 土星环 B、B'、短轴位置角、外缘长短轴,角度单位度,长短轴单位角秒
```
只需要中央经线时,可以单独调用 `CentralMeridians`
@@ -1555,8 +1615,8 @@ func main() {
2019-12-31 19:22:56.176710426 +0800 CST // 天狼星升起时刻
2020-01-01 05:30:39.834894239 +0800 CST // 天狼星落下时刻
大犬座 // 天狼星所在星座
5h58m10.19s // 织女一在公元 13600 年的赤经
84°1926.25″ // 织女一在公元 13600 年的赤纬
5h58m5.71s // 织女一在公元 13600 年的赤经
84°1926.13″ // 织女一在公元 13600 年的赤纬
天狼 Sirius -1.46 // 最亮恒星表第一项:中文名、英文常用名、视星等
```
@@ -1849,7 +1909,7 @@ func main() {
-`lite/sun``lite/moon` 轻量太阳/月亮链路:面向分钟级升落、轻量位置和月相计算
- ✅ 地球偏心率、日地距离、近日点、远日点
- ✅ 真平恒星时、星座计算、常用坐标转换、大气折射、大气质量、视差角、银道坐标
- ✅ 七大行星坐标、距日距地距离、特殊天象、物理星历、视直径、相位、视差角与节点
- ✅ 七大行星坐标、距日距地距离、特殊天象、水星/金星地心凌日、物理星历、视直径、相位、视差角与节点
- ✅ 公农历转换(公元前104年-公元3000年)
- ✅ 9100+恒星数据库
- ✅ 通用小天体轨道传播、H-G 视星等、视双星位置角/角距
+79
View File
@@ -0,0 +1,79 @@
package basic
import "math"
const (
exactEventTolerance = 2.0 / 86400.0
exactQueryTTToleranceUT = 0.1 / 86400.0
)
func sameEventJD(a, b float64) bool {
return math.Abs(a-b) <= exactEventTolerance
}
func sameEventUTQueryTT(eventUT, queryTT float64) bool {
return math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) <= exactQueryTTToleranceUT
}
func closestEventUTToQueryTT(queryTT, best float64, candidates ...float64) float64 {
bestAbs := math.Abs(eventUTQueryTTDelta(best, queryTT))
for _, candidate := range candidates {
candidateAbs := math.Abs(eventUTQueryTTDelta(candidate, queryTT))
if candidateAbs < bestAbs {
best = candidate
bestAbs = candidateAbs
}
}
return best
}
type phaseEventSearchFunc func(jde, degree float64, next uint8) float64
type simpleEventSearchFunc func(jde float64) float64
func inclusiveLastPhaseEvent(jde, degree float64, fn phaseEventSearchFunc) float64 {
last := fn(jde, degree, 0)
next := fn(jde, degree, 1)
if eventUTQueryBeforeOrEqual(next, jde) && eventUTQueryAfterOrEqual(next, jde) {
return next
}
if eventUTQueryBeforeOrEqual(last, jde) {
return last
}
return last
}
func inclusiveNextPhaseEvent(jde, degree float64, fn phaseEventSearchFunc) float64 {
last := fn(jde, degree, 0)
if eventUTQueryBeforeOrEqual(last, jde) && eventUTQueryAfterOrEqual(last, jde) {
return last
}
next := fn(jde, degree, 1)
if eventUTQueryAfterOrEqual(next, jde) {
return next
}
return next
}
func inclusiveLastSimpleEvent(jde float64, lastFn, nextFn simpleEventSearchFunc) float64 {
last := lastFn(jde)
next := nextFn(jde)
if eventUTQueryBeforeOrEqual(next, jde) && eventUTQueryAfterOrEqual(next, jde) {
return next
}
if eventUTQueryBeforeOrEqual(last, jde) {
return last
}
return last
}
func inclusiveNextSimpleEvent(jde float64, lastFn, nextFn simpleEventSearchFunc) float64 {
last := lastFn(jde)
if eventUTQueryBeforeOrEqual(last, jde) && eventUTQueryAfterOrEqual(last, jde) {
return last
}
next := nextFn(jde)
if eventUTQueryAfterOrEqual(next, jde) {
return next
}
return next
}
+1 -3
View File
@@ -40,9 +40,7 @@ func eventZeroBracket(leftJD, leftVal, centerJD, centerVal, rightJD, rightVal fl
return 0, 0, 0, 0, false
}
// eventZeroRefine 细化 seed 附近的零点;若找不到可用括号区间,则退回旧的固定步长扫描。
// eventZeroRefine refines a nearby zero crossing and falls back to the legacy
// fixed-step scan when no usable bracket is found.
// eventZeroRefine 细化 seed 附近的零点;可用括号区间时退回固定步长扫描。
func eventZeroRefine(seed, halfWindow, step float64, fn func(float64) float64) float64 {
leftJD := seed - halfWindow
centerJD := seed
+187
View File
@@ -0,0 +1,187 @@
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 TD2UT(queryTT, false)
}
func eventUTQueryTTDelta(eventUT, queryTT float64) float64 {
return eventUT - eventQueryTTAsUT(queryTT)
}
func eventUTQueryBeforeOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) <= innerEventEpsilon
}
func eventUTQueryAfterOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) >= -innerEventEpsilon
}
func eventUTNextQueryTT(eventUT float64) float64 {
return TD2UT(eventUT, true) + 1.0
}
func eventUTLastQueryTT(eventUT float64) float64 {
return TD2UT(eventUT, true) - 1.0
}
func innerNextCycleOffset(delta, period float64) float64 {
if delta <= 0 {
return -delta * period / 360.0
}
return (360.0 - delta) * period / 360.0
}
func innerLastCycleOffset(delta, period float64) float64 {
if delta >= 0 {
return delta * period / 360.0
}
return (360.0 + delta) * period / 360.0
}
func clampFloat64(v, min, max float64) float64 {
if v < min {
return min
}
if v > max {
return max
}
return v
}
func scanWindowForMinAbs(start, end, step float64, fn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if step <= 0 || end == start {
return start
}
bestJD := start
bestAbs := math.Abs(fn(start))
for jd := start + step; jd < end; jd += step {
candidateAbs := math.Abs(fn(jd))
if candidateAbs < bestAbs {
bestAbs = candidateAbs
bestJD = jd
}
}
endAbs := math.Abs(fn(end))
if endAbs < bestAbs {
return end
}
return bestJD
}
func scanWindowForMax(start, end, step float64, fn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if step <= 0 || end == start {
return start
}
bestJD := start
bestVal := fn(start)
for jd := start + step; jd < end; jd += step {
candidateVal := fn(jd)
if candidateVal > bestVal {
bestVal = candidateVal
bestJD = jd
}
}
endVal := fn(end)
if endVal > bestVal {
return end
}
return bestJD
}
func boundedEventZeroRefine(seed, start, end, halfWindow, step float64, fn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if end <= start {
return start
}
maxHalfWindow := (end - start) / 2
if halfWindow > maxHalfWindow {
halfWindow = maxHalfWindow
}
if halfWindow <= 0 {
return clampFloat64(seed, start, end)
}
seed = clampFloat64(seed, start+halfWindow, end-halfWindow)
return eventZeroRefine(seed, halfWindow, step, fn)
}
func zeroEventInWindow(start, end, coarseStep, halfWindow, refineStep float64, coarseFn, exactFn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if end <= start {
return start
}
rangeDays := end - start
if coarseStep <= 0 || coarseStep > rangeDays {
coarseStep = rangeDays / 6.0
}
if coarseStep < 0.5 {
coarseStep = 0.5
}
if refineStep <= 0 {
refineStep = 0.5 / 86400.0
}
if halfWindow <= 0 {
halfWindow = coarseStep
}
guess := scanWindowForMinAbs(start, end, coarseStep, coarseFn)
return boundedEventZeroRefine(guess, start, end, halfWindow, refineStep, exactFn)
}
func maximizeInWindow(start, end, coarseStep float64, coarseFn, exactFn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if end <= start {
return start
}
rangeDays := end - start
if coarseStep <= 0 || coarseStep > rangeDays {
coarseStep = rangeDays / 6.0
}
if coarseStep < 0.5 {
coarseStep = 0.5
}
guess := scanWindowForMax(start, end, coarseStep, coarseFn)
left := clampFloat64(guess-coarseStep, start, end)
right := clampFloat64(guess+coarseStep, start, end)
if right-left <= innerEventMaximizeEpsilon {
return guess
}
for i := 0; i < 20; i++ {
third := (right - left) / 3.0
leftThird := left + third
rightThird := right - third
if exactFn(leftThird) <= exactFn(rightThird) {
left = leftThird
continue
}
right = rightThird
}
bestJD := guess
bestVal := exactFn(bestJD)
for _, jd := range []float64{left, (left + right) / 2.0, right} {
candidateVal := exactFn(jd)
if candidateVal > bestVal {
bestVal = candidateVal
bestJD = jd
}
}
return bestJD
}
+105
View File
@@ -0,0 +1,105 @@
package basic
import (
"testing"
"time"
)
func TestInnerPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
cases := []struct {
name string
seed float64
lastFn func(float64) float64
nextFn func(float64) float64
}{
{name: "MercuryConjunction", seed: NextMercuryConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryConjunction, nextFn: NextMercuryConjunction},
{name: "MercuryInferior", seed: NextMercuryInferiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryInferiorConjunctionInclusive, nextFn: NextMercuryInferiorConjunctionInclusive},
{name: "MercurySuperior", seed: NextMercurySuperiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercurySuperiorConjunctionInclusive, nextFn: NextMercurySuperiorConjunctionInclusive},
{name: "MercuryRetrograde", seed: NextMercuryRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryRetrogradeInclusive, nextFn: NextMercuryRetrogradeInclusive},
{name: "MercuryP2R", seed: NextMercuryProgradeToRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryProgradeToRetrogradeInclusive, nextFn: NextMercuryProgradeToRetrogradeInclusive},
{name: "MercuryR2P", seed: NextMercuryRetrogradeToProgradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryRetrogradeToProgradeInclusive, nextFn: NextMercuryRetrogradeToProgradeInclusive},
{name: "MercuryGreatestElongation", seed: NextMercuryGreatestElongationInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryGreatestElongationInclusive, nextFn: NextMercuryGreatestElongationInclusive},
{name: "MercuryEastElongation", seed: NextMercuryGreatestElongationEastInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryGreatestElongationEastInclusive, nextFn: NextMercuryGreatestElongationEastInclusive},
{name: "MercuryWestElongation", seed: NextMercuryGreatestElongationWestInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryGreatestElongationWestInclusive, nextFn: NextMercuryGreatestElongationWestInclusive},
{name: "VenusConjunction", seed: NextVenusConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusConjunction, nextFn: NextVenusConjunction},
{name: "VenusInferior", seed: NextVenusInferiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusInferiorConjunctionInclusive, nextFn: NextVenusInferiorConjunctionInclusive},
{name: "VenusSuperior", seed: NextVenusSuperiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusSuperiorConjunctionInclusive, nextFn: NextVenusSuperiorConjunctionInclusive},
{name: "VenusRetrograde", seed: NextVenusRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusRetrogradeInclusive, nextFn: NextVenusRetrogradeInclusive},
{name: "VenusP2R", seed: NextVenusProgradeToRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusProgradeToRetrogradeInclusive, nextFn: NextVenusProgradeToRetrogradeInclusive},
{name: "VenusR2P", seed: NextVenusRetrogradeToProgradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusRetrogradeToProgradeInclusive, nextFn: NextVenusRetrogradeToProgradeInclusive},
{name: "VenusGreatestElongation", seed: NextVenusGreatestElongationInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusGreatestElongationInclusive, nextFn: NextVenusGreatestElongationInclusive},
{name: "VenusEastElongation", seed: NextVenusGreatestElongationEastInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusGreatestElongationEastInclusive, nextFn: NextVenusGreatestElongationEastInclusive},
{name: "VenusWestElongation", seed: NextVenusGreatestElongationWestInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusGreatestElongationWestInclusive, nextFn: NextVenusGreatestElongationWestInclusive},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
queryTT := TD2UT(tc.seed, true)
last := tc.lastFn(queryTT)
next := tc.nextFn(queryTT)
if !sameEventJD(last, tc.seed) {
t.Fatalf("last exact boundary mismatch: got %.12f want %.12f", last, tc.seed)
}
if !sameEventJD(next, tc.seed) {
t.Fatalf("next exact boundary mismatch: got %.12f want %.12f", next, tc.seed)
}
})
}
}
func 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 := TD2UT(Date2JDE(JDE2DateByZone(first, time.UTC, false).Add(time.Second)), true)
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 := TD2UT(tc.seed, true)
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)
}
})
}
}
+165
View File
@@ -0,0 +1,165 @@
package basic
import (
"encoding/json"
"os"
"strings"
"testing"
"time"
)
type innerBaselineFile struct {
Events []innerBaselineEvent `json:"events"`
}
type innerBaselineEvent struct {
Planet string `json:"planet"`
Kind string `json:"kind"`
NAOJHintJST string `json:"naoj_hint_jst"`
Precision string `json:"precision"`
CandidateJST string `json:"candidate_jst"`
VerifiedJST string `json:"verified_jst"`
CandidateSource string `json:"candidate_source"`
}
func loadInnerBaseline(t *testing.T) innerBaselineFile {
t.Helper()
paths := [][]string{
{
"testdata/jpl_inner_event_baseline.json",
"basic/testdata/jpl_inner_event_baseline.json",
},
{
"testdata/jpl_inner_event_baseline_21c_sample.json",
"basic/testdata/jpl_inner_event_baseline_21c_sample.json",
},
{
"testdata/jpl_inner_event_baseline_20c_sample.json",
"basic/testdata/jpl_inner_event_baseline_20c_sample.json",
},
{
"testdata/jpl_inner_event_baseline_22c_sample.json",
"basic/testdata/jpl_inner_event_baseline_22c_sample.json",
},
}
var merged innerBaselineFile
for index, candidates := range paths {
var (
data []byte
err error
)
for _, path := range candidates {
data, err = os.ReadFile(path)
if err == nil {
var baseline innerBaselineFile
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatal(err)
}
merged.Events = append(merged.Events, baseline.Events...)
break
}
}
if err != nil && index == 0 {
t.Fatal(err)
}
}
if len(merged.Events) == 0 {
t.Fatal("empty inner baseline file")
}
return merged
}
func parseInnerBaselineTime(t *testing.T, value string) time.Time {
t.Helper()
loc := time.FixedZone("JST", 9*3600)
layouts := []string{
"2006-01-02 15:04:05 MST",
"2006-01-02 15:04 MST",
"2006-01-02 15:04:05",
"2006-01-02 15:04",
}
var err error
for _, layout := range layouts {
when, parseErr := time.ParseInLocation(layout, value, loc)
if parseErr == nil {
return when
}
err = parseErr
}
t.Fatalf("parse baseline time %q: %v", value, err)
return time.Time{}
}
func innerBaselineTolerance(event innerBaselineEvent) time.Duration {
switch event.Kind {
case "IC", "SC", "P2R", "R2P":
return 2 * time.Minute
case "GEE", "GEW":
return 90 * time.Minute
default:
return 2 * time.Minute
}
}
func innerEventFuncs(t *testing.T, event innerBaselineEvent) (func(float64) float64, func(float64) float64) {
t.Helper()
switch event.Planet + ":" + event.Kind {
case "Mercury:IC":
return LastMercuryInferiorConjunctionInclusive, NextMercuryInferiorConjunctionInclusive
case "Mercury:SC":
return LastMercurySuperiorConjunctionInclusive, NextMercurySuperiorConjunctionInclusive
case "Mercury:P2R":
return LastMercuryProgradeToRetrogradeInclusive, NextMercuryProgradeToRetrogradeInclusive
case "Mercury:R2P":
return LastMercuryRetrogradeToProgradeInclusive, NextMercuryRetrogradeToProgradeInclusive
case "Mercury:GEE":
return LastMercuryGreatestElongationEastInclusive, NextMercuryGreatestElongationEastInclusive
case "Mercury:GEW":
return LastMercuryGreatestElongationWestInclusive, NextMercuryGreatestElongationWestInclusive
case "Venus:IC":
return LastVenusInferiorConjunctionInclusive, NextVenusInferiorConjunctionInclusive
case "Venus:SC":
return LastVenusSuperiorConjunctionInclusive, NextVenusSuperiorConjunctionInclusive
case "Venus:P2R":
return LastVenusProgradeToRetrogradeInclusive, NextVenusProgradeToRetrogradeInclusive
case "Venus:R2P":
return LastVenusRetrogradeToProgradeInclusive, NextVenusRetrogradeToProgradeInclusive
case "Venus:GEE":
return LastVenusGreatestElongationEastInclusive, NextVenusGreatestElongationEastInclusive
case "Venus:GEW":
return LastVenusGreatestElongationWestInclusive, NextVenusGreatestElongationWestInclusive
default:
t.Fatalf("unsupported event %s:%s", event.Planet, event.Kind)
return nil, nil
}
}
func assertInnerBaselineEvent(t *testing.T, event innerBaselineEvent, lastFn, nextFn func(float64) float64) {
t.Helper()
when := parseInnerBaselineTime(t, event.VerifiedJST)
before := when.Add(-24 * time.Hour)
after := when.Add(24 * time.Hour)
next := JDE2DateByZone(nextFn(toUTJD(before)), when.Location(), false)
last := JDE2DateByZone(lastFn(toUTJD(after)), when.Location(), false)
tolerance := innerBaselineTolerance(event)
if diff := next.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s next mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, next, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
if diff := last.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s last mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, last, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
}
func TestInnerPlanetTruthAgainstJPL(t *testing.T) {
baseline := loadInnerBaseline(t)
for _, event := range baseline.Events {
event := event
name := strings.Join([]string{event.Planet, event.Kind, event.VerifiedJST}, "_")
t.Run(name, func(t *testing.T) {
lastFn, nextFn := innerEventFuncs(t, event)
assertInnerBaselineEvent(t, event, lastFn, nextFn)
})
}
}
+7 -38
View File
@@ -87,53 +87,22 @@ func JupiterApparentRaDec(jd float64) (float64, float64) {
}
func EarthJupiterAway(jd float64) float64 {
x, y, z := AJupiterXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(4, jd, -1)
}
func JupiterApparentLo(jd float64) float64 {
x, y, z := AJupiterXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AJupiterXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(4, jd, -1)
return geo.lo
}
func JupiterApparentBo(jd float64) float64 {
x, y, z := AJupiterXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AJupiterXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(4, jd, -1)
return geo.bo
}
func JupiterApparentLoBo(jd float64) (float64, float64) {
x, y, z := AJupiterXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AJupiterXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo, bo
geo, _ := planetApparentGeocentricPositionN(4, jd, -1)
return geo.lo, geo.bo
}
func JupiterMag(jd float64) float64 {
+66 -65
View File
@@ -40,6 +40,28 @@ func jupiterSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64
return sub
}
func jupiterRADerivative(jde, delta float64) float64 {
sub := JupiterApparentRa(jde+delta) - JupiterApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func jupiterRADerivativeN(jde, delta float64, n int) float64 {
sub := JupiterApparentRaN(jde+delta, n) - JupiterApparentRaN(jde-delta, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func jupiterConjunctionFull(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := JUPITER_S_PERIOD / 360
@@ -94,113 +116,92 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 {
}
func LastJupiterConjunction(jde float64) float64 {
return jupiterConjunction(jde, 0, 0)
return inclusiveLastPhaseEvent(jde, 0, jupiterConjunction)
}
func NextJupiterConjunction(jde float64) float64 {
return jupiterConjunction(jde, 0, 1)
return inclusiveNextPhaseEvent(jde, 0, jupiterConjunction)
}
func LastJupiterOpposition(jde float64) float64 {
return jupiterConjunction(jde, 180, 0)
return inclusiveLastPhaseEvent(jde, 180, jupiterConjunction)
}
func NextJupiterOpposition(jde float64) float64 {
return jupiterConjunction(jde, 180, 1)
return inclusiveNextPhaseEvent(jde, 180, jupiterConjunction)
}
func NextJupiterEasternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 90, 1)
return inclusiveNextPhaseEvent(jde, 90, jupiterConjunction)
}
func LastJupiterEasternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 90, 0)
return inclusiveLastPhaseEvent(jde, 90, jupiterConjunction)
}
func NextJupiterWesternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 270, 1)
return inclusiveNextPhaseEvent(jde, 270, jupiterConjunction)
}
func LastJupiterWesternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 270, 0)
return inclusiveLastPhaseEvent(jde, 270, jupiterConjunction)
}
func jupiterRetrograde(jde float64, searchBeforeOpposition bool) float64 {
//0=last 1=next
raRate := func(jde float64, delta float64) float64 {
sub := JupiterApparentRa(jde+delta) - JupiterApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
jde = jupiterConjunctionFull(jde, 180, 1)
func jupiterRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
oppositionTT := TD2UT(oppositionJD, true)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
jde -= 60
easternQuadratureUT := jupiterConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
} else {
jde += 60
westernQuadratureUT := jupiterConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true)
}
for {
currentRate := raRate(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
rateValue := raRate(prevJD, 2.0/86400.0)
rateSlope := (raRate(prevJD+15.0/86400.0, 2.0/86400.0) - raRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return raRate(jd, 0.5/86400.0)
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return jupiterRADerivativeN(jd, 1.0/86400.0, jupiterEventSearchN)
}, func(jd float64) float64 {
return jupiterRADerivative(jd, 0.5/86400.0)
})
return TD2UT(bestJD, false)
}
func NextJupiterRetrogradeToPrograde(jde float64) float64 {
date := jupiterRetrograde(jde, false)
if date < jde {
oppositionJD := jupiterConjunctionFull(jde, 180, 1)
return jupiterRetrograde(oppositionJD+10, false)
}
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
return jupiterRetrogradeAroundOpposition(nextOppositionJD, false)
}
func LastJupiterRetrogradeToPrograde(jde float64) float64 {
jde = jupiterConjunctionFull(jde, 180, 0) - 10
date := jupiterRetrograde(jde, false)
if date > jde {
oppositionJD := jupiterConjunctionFull(jde, 180, 0)
return jupiterRetrograde(oppositionJD-10, false)
}
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
previousOppositionJD := jupiterConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return jupiterRetrogradeAroundOpposition(previousOppositionJD, false)
}
func NextJupiterProgradeToRetrograde(jde float64) float64 {
date := jupiterRetrograde(jde, true)
if date < jde {
oppositionJD := jupiterConjunctionFull(jde, 180, 1)
return jupiterRetrograde(oppositionJD+10, true)
}
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
followingOppositionJD := jupiterConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return jupiterRetrogradeAroundOpposition(followingOppositionJD, true)
}
func LastJupiterProgradeToRetrograde(jde float64) float64 {
jde = jupiterConjunctionFull(jde, 180, 0) - 10
date := jupiterRetrograde(jde, true)
if date > jde {
oppositionJD := jupiterConjunctionFull(jde, 180, 0)
return jupiterRetrograde(oppositionJD-10, true)
}
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
return jupiterRetrogradeAroundOpposition(lastOppositionJD, true)
}
+11 -51
View File
@@ -87,72 +87,32 @@ func MarsApparentRaDec(jd float64) (float64, float64) {
}
func EarthMarsAway(jd float64) float64 {
x, y, z := AMarsXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(3, jd, -1)
}
func MarsApparentLo(jd float64) float64 {
x, y, z := AMarsXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMarsXYZ(jd - to)
lo := math.Atan2(y, x)
//bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180.0 / math.Pi
//bo = bo * 180 / math.Pi
lo = Limit360(lo) + Nutation2000Bi(jd)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
return lo
geo, _ := planetApparentGeocentricPositionN(3, jd, -1)
return geo.lo
}
func MarsApparentBo(jd float64) float64 {
x, y, z := AMarsXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMarsXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(3, jd, -1)
return geo.bo
}
func MarsApparentLoBo(jd float64) (float64, float64) {
x, y, z := AMarsXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMarsXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo -= GXCLo(lo, bo, jd) / 3600
//bo += GXCBo(lo, bo, jd)
lo += Nutation2000Bi(jd)
return lo, bo
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
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
geo, _ := planetTrueGeocentricPositionN(3, jd, -1)
return geo.lo
}
func MarsMag(jd float64) float64 {
+80 -29
View File
@@ -116,40 +116,39 @@ func marsConjunction(jde, degree float64, next uint8) float64 {
}
func LastMarsConjunction(jde float64) float64 {
return marsConjunction(jde, 0, 0)
return inclusiveLastPhaseEvent(jde, 0, marsConjunction)
}
func NextMarsConjunction(jde float64) float64 {
return marsConjunction(jde, 0, 1)
return inclusiveNextPhaseEvent(jde, 0, marsConjunction)
}
func LastMarsOpposition(jde float64) float64 {
return marsConjunction(jde, 180, 0)
return inclusiveLastPhaseEvent(jde, 180, marsConjunction)
}
func NextMarsOpposition(jde float64) float64 {
return marsConjunction(jde, 180, 1)
return inclusiveNextPhaseEvent(jde, 180, marsConjunction)
}
func NextMarsEasternQuadrature(jde float64) float64 {
return marsConjunction(jde, 90, 1)
return inclusiveNextPhaseEvent(jde, 90, marsConjunction)
}
func LastMarsEasternQuadrature(jde float64) float64 {
return marsConjunction(jde, 90, 0)
return inclusiveLastPhaseEvent(jde, 90, marsConjunction)
}
func NextMarsWesternQuadrature(jde float64) float64 {
return marsConjunction(jde, 270, 1)
return inclusiveNextPhaseEvent(jde, 270, marsConjunction)
}
func LastMarsWesternQuadrature(jde float64) float64 {
return marsConjunction(jde, 270, 0)
return inclusiveLastPhaseEvent(jde, 270, marsConjunction)
}
func marsRetrograde(jde float64, searchBeforeOpposition bool) float64 {
//0=last 1=next
jde = marsConjunctionFull(jde, 180, 1)
func marsRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
jde := oppositionJD
if searchBeforeOpposition {
jde -= 60
} else {
@@ -179,40 +178,92 @@ func marsRetrograde(jde float64, searchBeforeOpposition bool) float64 {
return TD2UT(bestJD, false)
}
func marsOppositionFromBefore(oppositionJD float64) float64 {
return marsConjunctionFull(eventUTLastQueryTT(oppositionJD), 180, 1)
}
func marsOppositionFromAfter(oppositionJD float64) float64 {
return marsConjunctionFull(eventUTNextQueryTT(oppositionJD), 180, 0)
}
func stabilizeMarsStationNearQuery(jde, date float64, searchBeforeOpposition bool) float64 {
if math.Abs(eventUTQueryTTDelta(date, jde)) > exactEventTolerance {
return date
}
if searchBeforeOpposition {
stableOppositionJD := NextMarsOpposition(jde)
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(stableOppositionJD, true), marsRetrogradeAroundOpposition(sameOppositionJD, true))
}
stableOppositionJD := LastMarsOpposition(jde)
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(stableOppositionJD, false), marsRetrogradeAroundOpposition(sameOppositionJD, false))
}
func NextMarsRetrogradeToPrograde(jde float64) float64 {
date := marsRetrograde(jde, false)
if date < jde {
oppositionJD := marsConjunctionFull(jde, 180, 1)
return marsRetrograde(oppositionJD+10, false)
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
date = stabilizeMarsStationNearQuery(jde, date, false)
if sameEventUTQueryTT(date, jde) {
stableOppositionJD := LastMarsOpposition(jde)
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, false)
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, false))
}
if !eventUTQueryAfterOrEqual(date, jde) {
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
return marsRetrogradeAroundOpposition(nextOppositionJD, false)
}
return date
}
func LastMarsRetrogradeToPrograde(jde float64) float64 {
jde = marsConjunctionFull(jde, 180, 0) - 10
date := marsRetrograde(jde, false)
if date > jde {
oppositionJD := marsConjunctionFull(jde, 180, 0)
return marsRetrograde(oppositionJD-10, false)
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
date = stabilizeMarsStationNearQuery(jde, date, false)
if sameEventUTQueryTT(date, jde) {
stableOppositionJD := LastMarsOpposition(jde)
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, false)
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, false))
}
if !eventUTQueryBeforeOrEqual(date, jde) {
previousOppositionJD := marsConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return marsRetrogradeAroundOpposition(previousOppositionJD, false)
}
return date
}
func NextMarsProgradeToRetrograde(jde float64) float64 {
date := marsRetrograde(jde, true)
if date < jde {
oppositionJD := marsConjunctionFull(jde, 180, 1)
return marsRetrograde(oppositionJD+10, true)
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
date = stabilizeMarsStationNearQuery(jde, date, true)
if sameEventUTQueryTT(date, jde) {
stableOppositionJD := NextMarsOpposition(jde)
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, true)
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, true))
}
if !eventUTQueryAfterOrEqual(date, jde) {
followingOppositionJD := marsConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return marsRetrogradeAroundOpposition(followingOppositionJD, true)
}
return date
}
func LastMarsProgradeToRetrograde(jde float64) float64 {
jde = marsConjunctionFull(jde, 180, 0) - 10
date := marsRetrograde(jde, true)
if date > jde {
oppositionJD := marsConjunctionFull(jde, 180, 0)
return marsRetrograde(oppositionJD-10, true)
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
date = stabilizeMarsStationNearQuery(jde, date, true)
if sameEventUTQueryTT(date, jde) {
stableOppositionJD := NextMarsOpposition(jde)
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, true)
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, true))
}
if !eventUTQueryBeforeOrEqual(date, jde) {
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
return marsRetrogradeAroundOpposition(lastOppositionJD, true)
}
return date
}
+7 -37
View File
@@ -80,52 +80,22 @@ func MercuryApparentRaDec(jd float64) (float64, float64) {
}
func EarthMercuryAway(jd float64) float64 {
x, y, z := AMercuryXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(1, jd, -1)
}
func MercuryApparentLo(jd float64) float64 {
x, y, z := AMercuryXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMercuryXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(1, jd, -1)
return geo.lo
}
func MercuryApparentBo(jd float64) float64 {
x, y, z := AMercuryXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMercuryXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(1, jd, -1)
return geo.bo
}
func MercuryApparentLoBo(jd float64) (float64, float64) {
x, y, z := AMercuryXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMercuryXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo) + Nutation2000Bi(jd)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
return lo, bo
geo, _ := planetApparentGeocentricPositionN(1, jd, -1)
return geo.lo, geo.bo
}
func MercuryMag(jd float64) float64 {
+288 -90
View File
@@ -11,6 +11,7 @@ const (
MERCURY_S_PERIOD = 1 / ((1 / 87.9691) - (1 / 365.256363004))
mercuryConjunctionDerivativeStepDay = 2e-5 * 36525.0
mercuryLightTimeDaysPerAU = 0.0057755183
mercuryEventSearchN = 16
)
type mercuryConjunctionLBR struct {
@@ -167,6 +168,26 @@ func mercuryConjunctionLegacy(jde float64, next uint8) float64 {
func mercuryConjunction(jde float64, next uint8) float64 {
//0=last 1=next
if math.Abs(mercuryConjunctionExactDelta(jde)) <= 30.0/86400.0 {
best := math.NaN()
consider := func(inferior bool) {
eventUT := TD2UT(mercuryConjunctionExactTT(jde, inferior), false)
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
}
}
currentDelta := mercuryConjunctionExactDelta(jde)
// pos 大于0:远离太阳 小于0:靠近太阳
distanceTrend := math.Abs(mercuryConjunctionExactDelta(jde+1/86400.0)) - math.Abs(currentDelta)
@@ -206,41 +227,49 @@ func mercuryConjunction(jde float64, next uint8) float64 {
}
func LastMercuryConjunction(jde float64) float64 {
return mercuryConjunction(jde, 0)
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 := NextMercuryConjunction(jde)
date := NextMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) > EarthAway(date) {
return NextMercuryConjunction(date + 2)
return NextMercuryConjunctionStrict(date + 2)
}
return date
}
func NextMercurySuperiorConjunction(jde float64) float64 {
date := NextMercuryConjunction(jde)
date := NextMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) < EarthAway(date) {
return NextMercuryConjunction(date + 2)
return NextMercuryConjunctionStrict(date + 2)
}
return date
}
func LastMercuryInferiorConjunction(jde float64) float64 {
date := LastMercuryConjunction(jde)
date := LastMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) > EarthAway(date) {
return LastMercuryConjunction(date - 2)
return LastMercuryConjunctionStrict(date - 2)
}
return date
}
func LastMercurySuperiorConjunction(jde float64) float64 {
date := LastMercuryConjunction(jde)
date := LastMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) < EarthAway(date) {
return LastMercuryConjunction(date - 2)
return LastMercuryConjunctionStrict(date - 2)
}
return date
}
@@ -257,16 +286,6 @@ func mercuryRetrograde(jde float64) float64 {
}
return sub
}
raRate := func(jde float64, delta float64) float64 {
sub := MercuryApparentRa(jde+delta) - MercuryApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
lastConjunction := mercuryConjunctionLegacy(jde, 0)
nextConjunction := mercuryConjunctionLegacy(jde, 1)
currentRADelta := solarRADelta(jde)
@@ -276,7 +295,7 @@ func mercuryRetrograde(jde float64) float64 {
jde = lastConjunction + ((nextConjunction - lastConjunction) / 5.5)
}
for {
currentRate := raRate(jde, 1.0/86400.0)
currentRate := mercuryRADerivative(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
@@ -286,82 +305,212 @@ func mercuryRetrograde(jde float64) float64 {
estimateJD := jde
for {
prevJD := estimateJD
rateValue := raRate(prevJD, 2.0/86400.0)
rateSlope := (raRate(prevJD+15.0/86400.0, 2.0/86400.0) - raRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
rateValue := mercuryRADerivative(prevJD, 2.0/86400.0)
rateSlope := (mercuryRADerivative(prevJD+15.0/86400.0, 2.0/86400.0) - mercuryRADerivative(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return raRate(jd, 0.5/86400.0)
return mercuryRADerivative(jd, 0.5/86400.0)
})
//fmt.Println((bestJD - lastConjunction) / (nextConjunction - lastConjunction))
return TD2UT(bestJD, false)
}
func mercuryRADerivative(jde, delta float64) float64 {
sub := MercuryApparentRa(jde+delta) - MercuryApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func mercuryStationIsProgradeToRetrograde(eventUT float64) bool {
for _, offset := range []float64{0.25, 0.5, 1.0} {
before := mercuryRADerivative(eventUT-offset, 0.5/86400.0)
after := mercuryRADerivative(eventUT+offset, 0.5/86400.0)
if before > 0 && after < 0 {
return true
}
if before < 0 && after > 0 {
return false
}
}
before := mercuryRADerivative(eventUT-0.25, 0.5/86400.0)
after := mercuryRADerivative(eventUT+0.25, 0.5/86400.0)
return before > after
}
func nextMercuryTypedStation(jde float64, progradeToRetrograde bool) float64 {
date := NextMercuryRetrogradeStrict(jde)
for mercuryStationIsProgradeToRetrograde(date) != progradeToRetrograde {
date = NextMercuryRetrogradeStrict(eventUTNextQueryTT(date))
}
return date
}
func lastMercuryTypedStation(jde float64, progradeToRetrograde bool) float64 {
date := LastMercuryRetrogradeStrict(jde)
for mercuryStationIsProgradeToRetrograde(date) != progradeToRetrograde {
date = LastMercuryRetrogradeStrict(eventUTLastQueryTT(date))
}
return date
}
func NextMercuryRetrograde(jde float64) float64 {
date := mercuryRetrograde(jde)
if date < jde {
nextConjunction := mercuryConjunctionLegacy(jde, 1)
if !eventUTQueryAfterOrEqual(date, jde) {
nextConjunction := NextMercuryConjunctionStrict(jde)
return mercuryRetrograde(nextConjunction + 2)
}
return date
}
func LastMercuryRetrograde(jde float64) float64 {
lastConjunction := mercuryConjunctionLegacy(jde, 0)
lastConjunction := LastMercuryConjunctionStrict(jde)
date := mercuryRetrograde(lastConjunction + 2)
if date > jde {
previousConjunction := mercuryConjunctionLegacy(lastConjunction-2, 0)
if !eventUTQueryBeforeOrEqual(date, jde) {
previousConjunction := LastMercuryConjunctionStrict(eventUTLastQueryTT(lastConjunction))
return mercuryRetrograde(previousConjunction + 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)
func LastMercuryRetrogradeStrict(jde float64) float64 {
return LastMercuryRetrograde(jde)
}
return date
func NextMercuryRetrogradeStrict(jde float64) float64 {
return NextMercuryRetrograde(jde)
}
func NextMercuryProgradeToRetrograde(jde float64) float64 {
return nextMercuryTypedStation(jde, true)
}
func NextMercuryRetrogradeToPrograde(jde float64) float64 {
date := NextMercuryRetrograde(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return NextMercuryRetrograde(date + 12)
}
return date
return nextMercuryTypedStation(jde, false)
}
func LastMercuryProgradeToRetrograde(jde float64) float64 {
date := LastMercuryRetrograde(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return LastMercuryRetrograde(date - 12)
}
return date
return lastMercuryTypedStation(jde, true)
}
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
return lastMercuryTypedStation(jde, false)
}
func MercurySunElongation(jde float64) float64 {
lo1, bo1 := MercuryApparentLoBo(jde)
lo2 := SunApparentLo(jde)
lo2 := HSunApparentLo(jde)
bo2 := HSunTrueBo(jde)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
func mercurySunElongationN(jde float64, n int) float64 {
lo1, bo1 := MercuryApparentLoBoN(jde, n)
lo2 := HSunApparentLoN(jde, n)
bo2 := HSunTrueBoN(jde, n)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
func mercuryTrueElongationN(jde float64, n int) float64 {
earth := mercuryHelioN(-1, jde, n)
planetPos := mercuryHelioN(1, jde, n)
geo := mercuryGeocentric(planetPos, earth)
return StarAngularSeparation(geo.lo, geo.bo, HSunTrueLoN(jde, n), HSunTrueBoN(jde, n))
}
func mercuryGreatestElongationInWindow(start, end float64) float64 {
best := maximizeInWindow(start, end, 2.0, func(jd float64) float64 {
return mercuryTrueElongationN(jd, mercuryEventSearchN)
}, func(jd float64) float64 {
return mercuryTrueElongationN(jd, -1)
})
return TD2UT(best, false)
}
func mercuryEastElongationWindowEndingAt(inferior float64) (float64, float64) {
lastSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(inferior))
return lastSuperior + 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 east {
start, windowEnd := mercuryEastElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextInferior := NextMercuryInferiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = mercuryEastElongationWindowEndingAt(nextInferior)
}
}
start, windowEnd := mercuryWestElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextSuperior := NextMercurySuperiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = mercuryWestElongationWindowEndingAt(nextSuperior)
}
}
func lastMercuryGreatestElongationTyped(jde float64, east bool) float64 {
if east {
start, windowEnd := mercuryEastElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = mercuryEastElongationWindowEndingAt(prevInferior)
}
}
start, windowEnd := mercuryWestElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = mercuryWestElongationWindowEndingAt(prevSuperior)
}
}
func mercuryGreatestElongation(jde float64) float64 {
solarRADelta := func(jde float64) float64 {
sub := Limit360(MercuryApparentRa(jde) - SunApparentRa(jde))
@@ -383,8 +532,8 @@ func mercuryGreatestElongation(jde float64) float64 {
}
return sub / (2 * delta)
}
lastConjunction := mercuryConjunctionLegacy(jde, 0)
nextConjunction := mercuryConjunctionLegacy(jde, 1)
lastConjunction := LastMercuryConjunctionStrict(jde)
nextConjunction := NextMercuryConjunctionStrict(jde)
currentRADelta := solarRADelta(jde)
if currentRADelta > 0 {
jde = lastConjunction + ((nextConjunction - lastConjunction) / 5.0 * 2.0)
@@ -417,56 +566,105 @@ func mercuryGreatestElongation(jde float64) float64 {
}
func NextMercuryGreatestElongation(jde float64) float64 {
date := mercuryGreatestElongation(jde)
if date < jde {
nextConjunction := mercuryConjunctionLegacy(jde, 1)
return mercuryGreatestElongation(nextConjunction + 2)
east := NextMercuryGreatestElongationEast(jde)
west := NextMercuryGreatestElongationWest(jde)
if sameEventJD(east, west) {
return east
}
return date
if east < west {
return east
}
return west
}
func LastMercuryGreatestElongation(jde float64) float64 {
lastConjunction := mercuryConjunctionLegacy(jde, 0)
date := mercuryGreatestElongation(lastConjunction + 2)
if date > jde {
previousConjunction := mercuryConjunctionLegacy(lastConjunction-2, 0)
return mercuryGreatestElongation(previousConjunction + 2)
east := LastMercuryGreatestElongationEast(jde)
west := LastMercuryGreatestElongationWest(jde)
if sameEventJD(east, west) {
return east
}
return date
if east > west {
return east
}
return west
}
func LastMercuryInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryInferiorConjunction, NextMercuryInferiorConjunction)
}
func NextMercuryInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryInferiorConjunction, NextMercuryInferiorConjunction)
}
func LastMercurySuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercurySuperiorConjunction, NextMercurySuperiorConjunction)
}
func NextMercurySuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercurySuperiorConjunction, NextMercurySuperiorConjunction)
}
func LastMercuryRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryRetrograde, NextMercuryRetrograde)
}
func NextMercuryRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryRetrograde, NextMercuryRetrograde)
}
func LastMercuryProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryProgradeToRetrograde, NextMercuryProgradeToRetrograde)
}
func NextMercuryProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryProgradeToRetrograde, NextMercuryProgradeToRetrograde)
}
func LastMercuryRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryRetrogradeToPrograde, NextMercuryRetrogradeToPrograde)
}
func NextMercuryRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryRetrogradeToPrograde, NextMercuryRetrogradeToPrograde)
}
func LastMercuryGreatestElongationInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongation, NextMercuryGreatestElongation)
}
func NextMercuryGreatestElongationInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongation, NextMercuryGreatestElongation)
}
func LastMercuryGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongationEast, NextMercuryGreatestElongationEast)
}
func NextMercuryGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongationEast, NextMercuryGreatestElongationEast)
}
func LastMercuryGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongationWest, NextMercuryGreatestElongationWest)
}
func NextMercuryGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongationWest, NextMercuryGreatestElongationWest)
}
func NextMercuryGreatestElongationEast(jde float64) float64 {
date := NextMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return NextMercuryGreatestElongation(date + 1)
}
return date
return nextMercuryGreatestElongationTyped(jde, true)
}
func NextMercuryGreatestElongationWest(jde float64) float64 {
date := NextMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return NextMercuryGreatestElongation(date + 1)
}
return date
return nextMercuryGreatestElongationTyped(jde, false)
}
func LastMercuryGreatestElongationEast(jde float64) float64 {
date := LastMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return LastMercuryGreatestElongation(date - 1)
}
return date
return lastMercuryGreatestElongationTyped(jde, true)
}
func LastMercuryGreatestElongationWest(jde float64) float64 {
date := LastMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return LastMercuryGreatestElongation(date - 1)
}
return date
return lastMercuryGreatestElongationTyped(jde, false)
}
+40
View File
@@ -0,0 +1,40 @@
package basic
import (
"math"
"testing"
"time"
)
func mercuryTTJDJST(year int, month time.Month, day, hour, minute, second int) float64 {
loc := time.FixedZone("JST", 9*3600)
return TD2UT(Date2JDE(time.Date(year, month, day, hour, minute, second, 0, loc).UTC()), true)
}
func TestMercuryTypedStationRegression1929(t *testing.T) {
loc := time.FixedZone("JST", 9*3600)
const tolerance = 30.0 / 86400.0
query := mercuryTTJDJST(1929, time.September, 20, 0, 0, 0)
wantP2R := mercuryTTJDJST(1929, time.September, 26, 1, 58, 0)
wantR2P := mercuryTTJDJST(1929, time.October, 16, 23, 32, 33)
nextP2R := NextMercuryProgradeToRetrograde(query)
nextR2P := NextMercuryRetrogradeToPrograde(query)
if math.Abs(nextP2R-wantP2R) > tolerance {
t.Fatalf("next P2R mismatch: got %s want %s", JDE2DateByZone(nextP2R, loc, false), JDE2DateByZone(wantP2R, loc, false))
}
if math.Abs(nextR2P-wantR2P) > tolerance {
t.Fatalf("next R2P mismatch: got %s want %s", JDE2DateByZone(nextR2P, loc, false), JDE2DateByZone(wantR2P, loc, false))
}
query = mercuryTTJDJST(1929, time.October, 20, 0, 0, 0)
lastP2R := LastMercuryProgradeToRetrograde(query)
lastR2P := LastMercuryRetrogradeToPrograde(query)
if math.Abs(lastP2R-wantP2R) > tolerance {
t.Fatalf("last P2R mismatch: got %s want %s", JDE2DateByZone(lastP2R, loc, false), JDE2DateByZone(wantP2R, loc, false))
}
if math.Abs(lastR2P-wantR2P) > tolerance {
t.Fatalf("last R2P mismatch: got %s want %s", JDE2DateByZone(lastR2P, loc, false), JDE2DateByZone(wantR2P, loc, false))
}
}
@@ -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 := TD2UT(Date2JDE(date.UTC()), true)
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 := TD2UT(Date2JDE(sample.UTC()), true)
wantRA, wantDec := LoBoToRaDec(jd, HMoonTrueLo(jd), HMoonTrueBo(jd))
gotRA, gotDec := HMoonGeocentricTrueRaDec(jd)
assertPlanetApparentAngleClose(t, sample.Format(time.RFC3339)+".TrueRightAscension", gotRA, wantRA, 1e-12)
assertPlanetPhaseClose(t, sample.Format(time.RFC3339)+".TrueDeclination", gotDec, wantDec, 1e-12)
}
}
+30
View File
@@ -0,0 +1,30 @@
package basic
import (
"math"
"testing"
)
func TestHMoonGeocentricApparentRaDecComponentsMatch(t *testing.T) {
jd := TD2UT(JDECalc(2026, 1, 1.25), true)
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 := TD2UT(JDECalc(2026, 1, 1.25), true)
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))
}
}
+397
View File
@@ -0,0 +1,397 @@
package basic
import "math"
const (
moonPlanetConjunctionEstimateN = 8
moonPlanetConjunctionNearQueryDeltaDeg = 3.0
moonPlanetConjunctionDirectionEpsilon = 0.1 / 86400.0
moonPlanetConjunctionBracketStepDays = 0.5
moonPlanetConjunctionNearQueryStepDays = 0.25
moonPlanetConjunctionNearQueryHalfSpan = 1.5
moonPlanetConjunctionBracketHalfSpan = 2.0
moonPlanetConjunctionBracketGrowth = 2.0
moonPlanetConjunctionBracketAttempts = 3
moonPlanetConjunctionRefineStepDays = 0.5 / 86400.0
moonPlanetConjunctionEventTolerance = 0.01
moonPlanetConjunctionFallbackSpanScale = 1.5
)
type moonPlanetConjunctionLocalResult struct {
lastUT float64
nextUT float64
}
func emptyMoonPlanetConjunctionLocalResult() moonPlanetConjunctionLocalResult {
return moonPlanetConjunctionLocalResult{
lastUT: math.NaN(),
nextUT: math.NaN(),
}
}
// MoonPlanetConjunctionPlanet 月球合月目标行星 / target planet for Moon-planet conjunction events.
type MoonPlanetConjunctionPlanet int
const (
MoonPlanetConjunctionMercury MoonPlanetConjunctionPlanet = iota + 1
MoonPlanetConjunctionVenus
MoonPlanetConjunctionMars
MoonPlanetConjunctionJupiter
MoonPlanetConjunctionSaturn
MoonPlanetConjunctionUranus
MoonPlanetConjunctionNeptune
)
func moonPlanetConjunctionWrappedDelta(diff float64) float64 {
diff = math.Mod(diff+180, 360)
if diff < 0 {
diff += 360
}
return diff - 180
}
func moonPlanetConjunctionDeltaAt(jdTT float64, planet MoonPlanetConjunctionPlanet, n int) float64 {
moonRA := HMoonGeocentricApparentRaN(jdTT, n)
var planetRA float64
switch planet {
case MoonPlanetConjunctionMercury:
planetRA = MercuryApparentRaN(jdTT, n)
case MoonPlanetConjunctionVenus:
planetRA = VenusApparentRaN(jdTT, n)
case MoonPlanetConjunctionMars:
planetRA = MarsApparentRaN(jdTT, n)
case MoonPlanetConjunctionJupiter:
planetRA = JupiterApparentRaN(jdTT, n)
case MoonPlanetConjunctionSaturn:
planetRA = SaturnApparentRaN(jdTT, n)
case MoonPlanetConjunctionUranus:
planetRA = UranusApparentRaN(jdTT, n)
case MoonPlanetConjunctionNeptune:
planetRA = NeptuneApparentRaN(jdTT, n)
default:
return math.NaN()
}
return moonPlanetConjunctionWrappedDelta(moonRA - planetRA)
}
func moonPlanetConjunctionPeriodDays(planet MoonPlanetConjunctionPlanet) float64 {
switch planet {
case MoonPlanetConjunctionMercury:
return 28.1
case MoonPlanetConjunctionVenus:
return 28.4
case MoonPlanetConjunctionMars:
return 29.2
case MoonPlanetConjunctionJupiter:
return 28.0
case MoonPlanetConjunctionSaturn:
return 27.4
case MoonPlanetConjunctionUranus:
return 27.3
case MoonPlanetConjunctionNeptune:
return 27.3
default:
return math.NaN()
}
}
func moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) <= moonPlanetConjunctionDirectionEpsilon
}
func moonPlanetConjunctionAfterOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) >= -moonPlanetConjunctionDirectionEpsilon
}
func moonPlanetConjunctionInDirection(eventUT, queryTT float64, direction int) bool {
switch direction {
case -1:
return moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT)
case 1:
return moonPlanetConjunctionAfterOrEqual(eventUT, queryTT)
default:
return true
}
}
func moonPlanetConjunctionFindBracket(centerTT, halfSpan, step float64, planet MoonPlanetConjunctionPlanet) (float64, float64, bool) {
if math.IsNaN(centerTT) || math.IsNaN(halfSpan) || math.IsNaN(step) || halfSpan <= 0 || step <= 0 {
return 0, 0, false
}
start := centerTT - halfSpan
end := centerTT + halfSpan
samples := int(math.Ceil((end-start)/step)) + 1
prevTT := start
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1)
if math.IsNaN(prevVal) {
return 0, 0, false
}
if prevVal == 0 {
return prevTT, prevTT, true
}
bestLeft := 0.0
bestRight := 0.0
bestDistance := math.Inf(1)
for i := 1; i <= samples; i++ {
tt := start + float64(i)*step
if tt > end {
tt = end
}
val := moonPlanetConjunctionDeltaAt(tt, planet, -1)
if math.IsNaN(val) {
return 0, 0, false
}
if val == 0 {
return tt, tt, true
}
if prevVal*val < 0 {
mid := (prevTT + tt) / 2.0
distance := math.Abs(mid - centerTT)
if distance < bestDistance {
bestLeft = prevTT
bestRight = tt
bestDistance = distance
}
}
if tt == end {
break
}
prevTT = tt
prevVal = val
}
if math.IsInf(bestDistance, 1) {
return 0, 0, false
}
return bestLeft, bestRight, true
}
func moonPlanetConjunctionRefineBracket(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet) float64 {
if leftTT > rightTT {
leftTT, rightTT = rightTT, leftTT
}
if leftTT == rightTT {
return leftTT
}
center := (leftTT + rightTT) / 2.0
halfWindow := (rightTT - leftTT) / 2.0
return eventZeroRefine(center, halfWindow, moonPlanetConjunctionRefineStepDays, func(sampleTT float64) float64 {
return moonPlanetConjunctionDeltaAt(sampleTT, planet, -1)
})
}
func moonPlanetConjunctionEventUT(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet) float64 {
eventTT := moonPlanetConjunctionRefineBracket(leftTT, rightTT, planet)
if math.Abs(moonPlanetConjunctionDeltaAt(eventTT, planet, -1)) > moonPlanetConjunctionEventTolerance {
return math.NaN()
}
return TD2UT(eventTT, false)
}
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 := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true)
gotUT := tc.next(queryTT, tc.planet)
gotTime := JDE2DateByZone(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(TD2UT(gotUT, true), 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 := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true)
eventUT := NextMoonPlanetConjunction(seedTT, planet)
eventTime := JDE2DateByZone(eventUT, time.UTC, false)
queryAtTT := TD2UT(Date2JDE(eventTime.UTC()), true)
queryAfterTT := TD2UT(Date2JDE(eventTime.Add(time.Hour).UTC()), true)
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 := JDE2DateByZone(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 := TD2UT(Date2JDE(query), true)
lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
if math.Abs(lastUT-Date2JDE(query)) <= 5.0/86400.0 {
t.Fatalf("last returned query time on branch jump: got %s", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
}
if math.Abs(nextUT-Date2JDE(query)) <= 5.0/86400.0 {
t.Fatalf("next returned query time on branch jump: got %s", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano))
}
for name, gotUT := range map[string]float64{
"last": lastUT,
"next": nextUT,
} {
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), MoonPlanetConjunctionSaturn, -1))
if delta > moonPlanetConjunctionEventTolerance {
t.Fatalf("%s returned non-event candidate: delta=%.8f event=%s", name, delta, JDE2DateByZone(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 := TD2UT(Date2JDE(sample.query.UTC()), true)
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, JDE2DateByZone(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, JDE2DateByZone(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, JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(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(TD2UT(gotUT, true), sample.planet, -1))
if delta > moonPlanetConjunctionEventTolerance {
t.Fatalf("planet=%v %s returned non-event candidate: delta=%.8f event=%s", sample.planet, name, delta, JDE2DateByZone(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 := TD2UT(Date2JDE(query.UTC()), true)
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 - Date2JDE(wantLast)); diff > tolerance {
t.Fatalf("last mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), wantLast.Format(time.RFC3339Nano), diff*86400)
}
if diff := math.Abs(nextUT - Date2JDE(wantNext)); diff > tolerance {
t.Fatalf("next mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(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", JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
}
}
func TestMoonPlanetConjunctionNextAdvancesPastReturnedEvent(t *testing.T) {
seed := TD2UT(Date2JDE(time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)), true)
eventUT := NextMoonPlanetConjunction(seed, MoonPlanetConjunctionMercury)
query := JDE2DateByZone(eventUT, time.UTC, false).Add(time.Second)
queryTT := TD2UT(Date2JDE(query.UTC()), true)
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),
JDE2DateByZone(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",
JDE2DateByZone(eventUT, time.UTC, false).Format(time.RFC3339Nano),
JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
)
}
}
+62
View File
@@ -126,6 +126,68 @@ func HMoonApparentLoN(jd float64, n int) float64 {
return HMoonTrueLoN(jd, n) + Nutation2000Bi(jd)
}
// HMoonGeocentricApparentRa 月亮地心视赤经 / apparent geocentric right ascension of the Moon.
func HMoonGeocentricApparentRa(jd float64) float64 {
return HMoonGeocentricApparentRaN(jd, -1)
}
// HMoonGeocentricApparentRaN 月亮地心视赤经(截断版) / truncated apparent geocentric right ascension of the Moon.
func HMoonGeocentricApparentRaN(jd float64, n int) float64 {
return LoToRa(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n))
}
// HMoonGeocentricApparentDec 月亮地心视赤纬 / apparent geocentric declination of the Moon.
func HMoonGeocentricApparentDec(jd float64) float64 {
return HMoonGeocentricApparentDecN(jd, -1)
}
// HMoonGeocentricApparentDecN 月亮地心视赤纬(截断版) / truncated apparent geocentric declination of the Moon.
func HMoonGeocentricApparentDecN(jd float64, n int) float64 {
return ArcSin(Sin(HMoonTrueBoN(jd, n))*Cos(TrueObliquity(jd)) +
Cos(HMoonTrueBoN(jd, n))*Sin(TrueObliquity(jd))*Sin(HMoonApparentLoN(jd, n)))
}
// HMoonGeocentricApparentRaDec 月亮地心视赤经、视赤纬 / apparent geocentric right ascension and declination of the Moon.
func HMoonGeocentricApparentRaDec(jd float64) (float64, float64) {
return HMoonGeocentricApparentRaDecN(jd, -1)
}
// HMoonGeocentricApparentRaDecN 月亮地心视赤经、视赤纬(截断版) / truncated apparent geocentric right ascension and declination of the Moon.
func HMoonGeocentricApparentRaDecN(jd float64, n int) (float64, float64) {
return LoBoToRaDec(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n))
}
// HMoonGeocentricTrueRa 月亮地心真赤经 / true geocentric right ascension of the Moon.
func HMoonGeocentricTrueRa(jd float64) float64 {
return HMoonGeocentricTrueRaN(jd, -1)
}
// HMoonGeocentricTrueRaN 月亮地心真赤经(截断版) / truncated true geocentric right ascension of the Moon.
func HMoonGeocentricTrueRaN(jd float64, n int) float64 {
return LoToRa(jd, HMoonTrueLoN(jd, n), HMoonTrueBoN(jd, n))
}
// HMoonGeocentricTrueDec 月亮地心真赤纬 / true geocentric declination of the Moon.
func HMoonGeocentricTrueDec(jd float64) float64 {
return HMoonGeocentricTrueDecN(jd, -1)
}
// HMoonGeocentricTrueDecN 月亮地心真赤纬(截断版) / truncated true geocentric declination of the Moon.
func HMoonGeocentricTrueDecN(jd float64, n int) float64 {
return ArcSin(Sin(HMoonTrueBoN(jd, n))*Cos(TrueObliquity(jd)) +
Cos(HMoonTrueBoN(jd, n))*Sin(TrueObliquity(jd))*Sin(HMoonTrueLoN(jd, n)))
}
// HMoonGeocentricTrueRaDec 月亮地心真赤经、真赤纬 / true geocentric right ascension and declination of the Moon.
func HMoonGeocentricTrueRaDec(jd float64) (float64, float64) {
return HMoonGeocentricTrueRaDecN(jd, -1)
}
// HMoonGeocentricTrueRaDecN 月亮地心真赤经、真赤纬(截断版) / truncated true geocentric right ascension and declination of the Moon.
func HMoonGeocentricTrueRaDecN(jd float64, n int) (float64, float64) {
return LoBoToRaDec(jd, HMoonTrueLoN(jd, n), HMoonTrueBoN(jd, n))
}
func HMoonTrueRaDec(jd float64) (float64, float64) {
return HMoonTrueRaDecN(jd, -1)
}
+7 -38
View File
@@ -87,53 +87,22 @@ func NeptuneApparentRaDec(jd float64) (float64, float64) {
}
func EarthNeptuneAway(jd float64) float64 {
x, y, z := ANeptuneXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(7, jd, -1)
}
func NeptuneApparentLo(jd float64) float64 {
x, y, z := ANeptuneXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ANeptuneXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(7, jd, -1)
return geo.lo
}
func NeptuneApparentBo(jd float64) float64 {
x, y, z := ANeptuneXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ANeptuneXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(7, jd, -1)
return geo.bo
}
func NeptuneApparentLoBo(jd float64) (float64, float64) {
x, y, z := ANeptuneXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ANeptuneXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo, bo
geo, _ := planetApparentGeocentricPositionN(7, jd, -1)
return geo.lo, geo.bo
}
func NeptuneMag(jd float64) float64 {
+67 -66
View File
@@ -9,7 +9,7 @@ import (
// Pos
const (
NEPTUNE_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 4332.59))
NEPTUNE_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 60190.03))
neptuneEventSearchN = 16
neptunePhaseCoarseTolerance = 30.0 / 86400.0
)
@@ -40,6 +40,28 @@ func neptuneSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64
return sub
}
func neptuneRADerivative(jde, delta float64) float64 {
sub := NeptuneApparentRa(jde+delta) - NeptuneApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func neptuneRADerivativeN(jde, delta float64, n int) float64 {
sub := NeptuneApparentRaN(jde+delta, n) - NeptuneApparentRaN(jde-delta, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func neptuneConjunctionFull(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := NEPTUNE_S_PERIOD / 360
@@ -94,113 +116,92 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 {
}
func LastNeptuneConjunction(jde float64) float64 {
return neptuneConjunction(jde, 0, 0)
return inclusiveLastPhaseEvent(jde, 0, neptuneConjunction)
}
func NextNeptuneConjunction(jde float64) float64 {
return neptuneConjunction(jde, 0, 1)
return inclusiveNextPhaseEvent(jde, 0, neptuneConjunction)
}
func LastNeptuneOpposition(jde float64) float64 {
return neptuneConjunction(jde, 180, 0)
return inclusiveLastPhaseEvent(jde, 180, neptuneConjunction)
}
func NextNeptuneOpposition(jde float64) float64 {
return neptuneConjunction(jde, 180, 1)
return inclusiveNextPhaseEvent(jde, 180, neptuneConjunction)
}
func NextNeptuneEasternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 90, 1)
return inclusiveNextPhaseEvent(jde, 90, neptuneConjunction)
}
func LastNeptuneEasternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 90, 0)
return inclusiveLastPhaseEvent(jde, 90, neptuneConjunction)
}
func NextNeptuneWesternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 270, 1)
return inclusiveNextPhaseEvent(jde, 270, neptuneConjunction)
}
func LastNeptuneWesternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 270, 0)
return inclusiveLastPhaseEvent(jde, 270, neptuneConjunction)
}
func neptuneRetrograde(jde float64, searchBeforeOpposition bool) float64 {
//0=last 1=next
raRate := func(jde float64, delta float64) float64 {
sub := NeptuneApparentRa(jde+delta) - NeptuneApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
jde = neptuneConjunctionFull(jde, 180, 1)
func neptuneRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
oppositionTT := TD2UT(oppositionJD, true)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
jde -= 60
easternQuadratureUT := neptuneConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
} else {
jde += 60
westernQuadratureUT := neptuneConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true)
}
for {
currentRate := raRate(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
rateValue := raRate(prevJD, 2.0/86400.0)
rateSlope := (raRate(prevJD+15.0/86400.0, 2.0/86400.0) - raRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return raRate(jd, 0.5/86400.0)
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return neptuneRADerivativeN(jd, 1.0/86400.0, neptuneEventSearchN)
}, func(jd float64) float64 {
return neptuneRADerivative(jd, 0.5/86400.0)
})
return TD2UT(bestJD, false)
}
func NextNeptuneRetrogradeToPrograde(jde float64) float64 {
date := neptuneRetrograde(jde, false)
if date < jde {
oppositionJD := neptuneConjunctionFull(jde, 180, 1)
return neptuneRetrograde(oppositionJD+10, false)
}
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
date := neptuneRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
return neptuneRetrogradeAroundOpposition(nextOppositionJD, false)
}
func LastNeptuneRetrogradeToPrograde(jde float64) float64 {
jde = neptuneConjunctionFull(jde, 180, 0) - 10
date := neptuneRetrograde(jde, false)
if date > jde {
oppositionJD := neptuneConjunctionFull(jde, 180, 0)
return neptuneRetrograde(oppositionJD-10, false)
}
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
date := neptuneRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
previousOppositionJD := neptuneConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return neptuneRetrogradeAroundOpposition(previousOppositionJD, false)
}
func NextNeptuneProgradeToRetrograde(jde float64) float64 {
date := neptuneRetrograde(jde, true)
if date < jde {
oppositionJD := neptuneConjunctionFull(jde, 180, 1)
return neptuneRetrograde(oppositionJD+10, true)
}
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
date := neptuneRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
followingOppositionJD := neptuneConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return neptuneRetrogradeAroundOpposition(followingOppositionJD, true)
}
func LastNeptuneProgradeToRetrograde(jde float64) float64 {
jde = neptuneConjunctionFull(jde, 180, 0) - 10
date := neptuneRetrograde(jde, true)
if date > jde {
oppositionJD := neptuneConjunctionFull(jde, 180, 0)
return neptuneRetrograde(oppositionJD-10, true)
}
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
date := neptuneRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
return neptuneRetrogradeAroundOpposition(lastOppositionJD, true)
}
+84
View File
@@ -0,0 +1,84 @@
package basic
import (
"testing"
"time"
)
func TestOuterPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
cases := []struct {
name string
seed float64
lastFn func(float64) float64
nextFn func(float64) float64
}{
{name: "JupiterConjunction", seed: NextJupiterConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterConjunction, nextFn: NextJupiterConjunction},
{name: "JupiterOpposition", seed: NextJupiterOpposition(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterOpposition, nextFn: NextJupiterOpposition},
{name: "JupiterEasternQuadrature", seed: NextJupiterEasternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterEasternQuadrature, nextFn: NextJupiterEasternQuadrature},
{name: "JupiterWesternQuadrature", seed: NextJupiterWesternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterWesternQuadrature, nextFn: NextJupiterWesternQuadrature},
{name: "JupiterP2R", seed: NextJupiterProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterProgradeToRetrograde, nextFn: NextJupiterProgradeToRetrograde},
{name: "JupiterR2P", seed: NextJupiterRetrogradeToPrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterRetrogradeToPrograde, nextFn: NextJupiterRetrogradeToPrograde},
{name: "SaturnOpposition", seed: NextSaturnOpposition(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastSaturnOpposition, nextFn: NextSaturnOpposition},
{name: "SaturnP2R", seed: NextSaturnProgradeToRetrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastSaturnProgradeToRetrograde, nextFn: NextSaturnProgradeToRetrograde},
{name: "SaturnR2P", seed: NextSaturnRetrogradeToPrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastSaturnRetrogradeToPrograde, nextFn: NextSaturnRetrogradeToPrograde},
{name: "UranusOpposition", seed: NextUranusOpposition(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastUranusOpposition, nextFn: NextUranusOpposition},
{name: "UranusP2R", seed: NextUranusProgradeToRetrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastUranusProgradeToRetrograde, nextFn: NextUranusProgradeToRetrograde},
{name: "UranusR2P", seed: NextUranusRetrogradeToPrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastUranusRetrogradeToPrograde, nextFn: NextUranusRetrogradeToPrograde},
{name: "NeptuneOpposition", seed: NextNeptuneOpposition(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastNeptuneOpposition, nextFn: NextNeptuneOpposition},
{name: "NeptuneP2R", seed: NextNeptuneProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastNeptuneProgradeToRetrograde, nextFn: NextNeptuneProgradeToRetrograde},
{name: "NeptuneR2P", seed: NextNeptuneRetrogradeToPrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastNeptuneRetrogradeToPrograde, nextFn: NextNeptuneRetrogradeToPrograde},
{name: "MarsConjunction", seed: NextMarsConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsConjunction, nextFn: NextMarsConjunction},
{name: "MarsOpposition", seed: NextMarsOpposition(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsOpposition, nextFn: NextMarsOpposition},
{name: "MarsEasternQuadrature", seed: NextMarsEasternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsEasternQuadrature, nextFn: NextMarsEasternQuadrature},
{name: "MarsWesternQuadrature", seed: NextMarsWesternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsWesternQuadrature, nextFn: NextMarsWesternQuadrature},
{name: "MarsP2R", seed: NextMarsProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsProgradeToRetrograde, nextFn: NextMarsProgradeToRetrograde},
{name: "MarsR2P", seed: NextMarsRetrogradeToPrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastMarsRetrogradeToPrograde, nextFn: NextMarsRetrogradeToPrograde},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
queryTT := TD2UT(tc.seed, true)
last := tc.lastFn(queryTT)
next := tc.nextFn(queryTT)
if !sameEventJD(last, tc.seed) {
t.Fatalf("last exact boundary mismatch: got %.12f want %.12f", last, tc.seed)
}
if !sameEventJD(next, tc.seed) {
t.Fatalf("next exact boundary mismatch: got %.12f want %.12f", next, tc.seed)
}
})
}
}
func TestOuterPlanetNextEventAdvancesPastReturnedEvent(t *testing.T) {
cases := []struct {
name string
seed float64
next func(float64) float64
}{
{name: "MarsOpposition", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextMarsOpposition},
{name: "MarsP2R", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextMarsProgradeToRetrograde},
{name: "JupiterOpposition", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextJupiterOpposition},
{name: "SaturnConjunction", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextSaturnConjunction},
{name: "UranusP2R", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextUranusProgradeToRetrograde},
{name: "NeptuneR2P", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextNeptuneRetrogradeToPrograde},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
first := tc.next(tc.seed)
query := TD2UT(Date2JDE(JDE2DateByZone(first, time.UTC, false).Add(time.Second)), true)
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 ttjdUTC(year, month, day, hour, min, sec int) float64 {
return TD2UT(Date2JDE(time.Date(year, time.Month(month), day, hour, min, sec, 0, time.UTC)), true)
}
+181
View File
@@ -0,0 +1,181 @@
package basic
import (
"encoding/json"
"os"
"strings"
"testing"
"time"
)
type outerTruthBaselineFile struct {
Events []outerTruthBaselineEvent `json:"events"`
}
type outerTruthBaselineEvent struct {
Planet string `json:"planet"`
Kind string `json:"kind"`
HintKind string `json:"hint_kind"`
NAOJHintJST string `json:"naoj_hint_jst"`
Precision string `json:"precision"`
CandidateJST string `json:"candidate_jst"`
VerifiedJST string `json:"verified_jst"`
CandidateSource string `json:"candidate_source"`
}
func loadOuterTruthBaseline(t *testing.T) outerTruthBaselineFile {
t.Helper()
paths := [][]string{
{
"testdata/jpl_outer_event_baseline.json",
"basic/testdata/jpl_outer_event_baseline.json",
},
{
"testdata/jpl_outer_event_baseline_21c_sample.json",
"basic/testdata/jpl_outer_event_baseline_21c_sample.json",
},
}
var merged outerTruthBaselineFile
for index, candidates := range paths {
var (
data []byte
err error
)
for _, path := range candidates {
data, err = os.ReadFile(path)
if err == nil {
var baseline outerTruthBaselineFile
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatal(err)
}
merged.Events = append(merged.Events, baseline.Events...)
break
}
}
if err != nil && index == 0 {
t.Fatal(err)
}
}
if len(merged.Events) == 0 {
t.Fatal("empty outer truth baseline file")
}
return merged
}
func outerTruthTolerance(event outerTruthBaselineEvent) time.Duration {
switch event.Kind {
case "CONJ", "OPP", "EQE", "EQW":
return 2 * time.Minute
default:
return 2 * time.Minute
}
}
func outerTruthEventFuncs(t *testing.T, event outerTruthBaselineEvent) (func(float64) float64, func(float64) float64) {
t.Helper()
switch event.Planet + ":" + event.Kind {
case "Jupiter:CONJ":
return LastJupiterConjunction, NextJupiterConjunction
case "Jupiter:OPP":
return LastJupiterOpposition, NextJupiterOpposition
case "Jupiter:EQE":
return LastJupiterEasternQuadrature, NextJupiterEasternQuadrature
case "Jupiter:EQW":
return LastJupiterWesternQuadrature, NextJupiterWesternQuadrature
case "Jupiter:P2R":
return LastJupiterProgradeToRetrograde, NextJupiterProgradeToRetrograde
case "Jupiter:R2P":
return LastJupiterRetrogradeToPrograde, NextJupiterRetrogradeToPrograde
case "Saturn:CONJ":
return LastSaturnConjunction, NextSaturnConjunction
case "Saturn:OPP":
return LastSaturnOpposition, NextSaturnOpposition
case "Saturn:EQE":
return LastSaturnEasternQuadrature, NextSaturnEasternQuadrature
case "Saturn:EQW":
return LastSaturnWesternQuadrature, NextSaturnWesternQuadrature
case "Saturn:P2R":
return LastSaturnProgradeToRetrograde, NextSaturnProgradeToRetrograde
case "Saturn:R2P":
return LastSaturnRetrogradeToPrograde, NextSaturnRetrogradeToPrograde
case "Uranus:CONJ":
return LastUranusConjunction, NextUranusConjunction
case "Uranus:OPP":
return LastUranusOpposition, NextUranusOpposition
case "Uranus:EQE":
return LastUranusEasternQuadrature, NextUranusEasternQuadrature
case "Uranus:EQW":
return LastUranusWesternQuadrature, NextUranusWesternQuadrature
case "Uranus:P2R":
return LastUranusProgradeToRetrograde, NextUranusProgradeToRetrograde
case "Uranus:R2P":
return LastUranusRetrogradeToPrograde, NextUranusRetrogradeToPrograde
case "Neptune:CONJ":
return LastNeptuneConjunction, NextNeptuneConjunction
case "Neptune:OPP":
return LastNeptuneOpposition, NextNeptuneOpposition
case "Neptune:EQE":
return LastNeptuneEasternQuadrature, NextNeptuneEasternQuadrature
case "Neptune:EQW":
return LastNeptuneWesternQuadrature, NextNeptuneWesternQuadrature
case "Neptune:P2R":
return LastNeptuneProgradeToRetrograde, NextNeptuneProgradeToRetrograde
case "Neptune:R2P":
return LastNeptuneRetrogradeToPrograde, NextNeptuneRetrogradeToPrograde
default:
t.Fatalf("unsupported outer event %s:%s", event.Planet, event.Kind)
return nil, nil
}
}
func assertOuterTruthBaselineEvent(t *testing.T, event outerTruthBaselineEvent, lastFn, nextFn func(float64) float64) {
t.Helper()
when := parseInnerBaselineTime(t, event.VerifiedJST)
before := when.Add(-7 * 24 * time.Hour)
after := when.Add(7 * 24 * time.Hour)
next := JDE2DateByZone(nextFn(toUTJD(before)), when.Location(), false)
last := JDE2DateByZone(lastFn(toUTJD(after)), when.Location(), false)
tolerance := outerTruthTolerance(event)
if diff := next.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s next mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, next, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
if diff := last.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s last mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, last, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
}
func TestOuterPlanetPhaseTruthAgainstJPL(t *testing.T) {
baseline := loadOuterTruthBaseline(t)
for _, event := range baseline.Events {
event := event
switch event.Kind {
case "P2R", "R2P":
// Station rows are retained as JPL apparent-RA reference data for
// future refinement. Current station behavior is constrained by the
// library's existing station baseline instead of these reference rows.
continue
}
name := strings.Join([]string{event.Planet, event.Kind, event.VerifiedJST}, "_")
t.Run(name, func(t *testing.T) {
lastFn, nextFn := outerTruthEventFuncs(t, event)
assertOuterTruthBaselineEvent(t, event, lastFn, nextFn)
})
}
}
func TestOuterPlanetStationJPLReferenceLoaded(t *testing.T) {
baseline := loadOuterTruthBaseline(t)
count := 0
for _, event := range baseline.Events {
switch event.Kind {
case "P2R", "R2P":
count++
}
}
if count == 0 {
t.Fatal("missing outer station JPL reference rows")
}
}
+84
View File
@@ -0,0 +1,84 @@
package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
type planetGeocentricPosition struct {
x float64
y float64
z float64
lo float64
bo float64
}
func planetHeliocentricXYZN(planetIndex int, jd float64, n int) (float64, float64, float64) {
l := planet.WherePlanetN(planetIndex, 0, jd, n)
b := planet.WherePlanetN(planetIndex, 1, jd, n)
r := planet.WherePlanetN(planetIndex, 2, jd, n)
return sphericalToRectangular(l, b, r)
}
func earthHeliocentricXYZN(jd float64, n int) (float64, float64, float64) {
l := planet.WherePlanetN(-1, 0, jd, n)
b := planet.WherePlanetN(-1, 1, jd, n)
r := planet.WherePlanetN(-1, 2, jd, n)
return sphericalToRectangular(l, b, r)
}
func sphericalToRectangular(lo, bo, radius float64) (float64, float64, float64) {
cosBo := math.Cos(bo * math.Pi / 180)
return radius * cosBo * math.Cos(lo*math.Pi/180),
radius * cosBo * math.Sin(lo*math.Pi/180),
radius * math.Sin(bo*math.Pi/180)
}
func geocentricPositionFromRectangular(x, y, z float64) planetGeocentricPosition {
lo := math.Atan2(y, x) * 180 / math.Pi
bo := math.Atan2(z, math.Sqrt(x*x+y*y)) * 180 / math.Pi
return planetGeocentricPosition{
x: x,
y: y,
z: z,
lo: Limit360(lo),
bo: bo,
}
}
func planetGeocentricPositionN(planetIndex int, planetJD, earthJD float64, n int) planetGeocentricPosition {
px, py, pz := planetHeliocentricXYZN(planetIndex, planetJD, n)
ex, ey, ez := earthHeliocentricXYZN(earthJD, n)
return geocentricPositionFromRectangular(px-ex, py-ey, pz-ez)
}
func planetGeocentricPositionWithEarthN(planetIndex int, planetJD float64, ex, ey, ez float64, n int) planetGeocentricPosition {
px, py, pz := planetHeliocentricXYZN(planetIndex, planetJD, n)
return geocentricPositionFromRectangular(px-ex, py-ey, pz-ez)
}
func planetApparentGeocentricPositionN(planetIndex int, jd float64, n int) (planetGeocentricPosition, float64) {
ex, ey, ez := earthHeliocentricXYZN(jd, n)
geoNow := planetGeocentricPositionWithEarthN(planetIndex, jd, ex, ey, ez, n)
tau := 0.0057755183 * math.Sqrt(geoNow.x*geoNow.x+geoNow.y*geoNow.y+geoNow.z*geoNow.z)
geo := planetGeocentricPositionWithEarthN(planetIndex, jd-tau, ex, ey, ez, n)
baseLo := geo.lo
baseBo := geo.bo
geo.lo = Limit360(baseLo + GXCLo(baseLo, baseBo, jd)/3600.0 + Nutation2000Bi(jd))
geo.bo = baseBo + GXCBo(baseLo, baseBo, jd)/3600.0
return geo, tau
}
func planetTrueGeocentricPositionN(planetIndex int, jd float64, n int) (planetGeocentricPosition, float64) {
ex, ey, ez := earthHeliocentricXYZN(jd, n)
geoNow := planetGeocentricPositionWithEarthN(planetIndex, jd, ex, ey, ez, n)
tau := 0.0057755183 * math.Sqrt(geoNow.x*geoNow.x+geoNow.y*geoNow.y+geoNow.z*geoNow.z)
return planetGeocentricPositionWithEarthN(planetIndex, jd-tau, ex, ey, ez, n), tau
}
func planetEarthAwayExplicitN(planetIndex int, jd float64, n int) float64 {
geoNow := planetGeocentricPositionN(planetIndex, jd, jd, n)
return math.Sqrt(geoNow.x*geoNow.x + geoNow.y*geoNow.y + geoNow.z*geoNow.z)
}
+83
View File
@@ -0,0 +1,83 @@
package basic
import (
"encoding/json"
"os"
"testing"
"time"
)
type planetApparentSample struct {
Body string `json:"body"`
InputUTC string `json:"input_utc"`
RightAscension float64 `json:"right_ascension"`
Declination float64 `json:"declination"`
EclipticLongitude float64 `json:"ecliptic_longitude"`
EclipticLatitude float64 `json:"ecliptic_latitude"`
}
func TestPlanetApparentCoordinatesMatchHorizonsBaseline(t *testing.T) {
data, err := os.ReadFile("testdata/planet_apparent_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []planetApparentSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
type apparentCase struct {
lo func(float64) float64
bo func(float64) float64
ra func(float64) float64
dec func(float64) float64
}
cases := map[string]apparentCase{
"mercury": {lo: MercuryApparentLo, bo: MercuryApparentBo, ra: MercuryApparentRa, dec: MercuryApparentDec},
"venus": {lo: VenusApparentLo, bo: VenusApparentBo, ra: VenusApparentRa, dec: VenusApparentDec},
"mars": {lo: MarsApparentLo, bo: MarsApparentBo, ra: MarsApparentRa, dec: MarsApparentDec},
"jupiter": {lo: JupiterApparentLo, bo: JupiterApparentBo, ra: JupiterApparentRa, dec: JupiterApparentDec},
"saturn": {lo: SaturnApparentLo, bo: SaturnApparentBo, ra: SaturnApparentRa, dec: SaturnApparentDec},
"uranus": {lo: UranusApparentLo, bo: UranusApparentBo, ra: UranusApparentRa, dec: UranusApparentDec},
"neptune": {lo: NeptuneApparentLo, bo: NeptuneApparentBo, ra: NeptuneApparentRa, dec: NeptuneApparentDec},
}
seen := make(map[string]bool, len(cases))
for _, sample := range samples {
tc, ok := cases[sample.Body]
if !ok {
t.Fatalf("unknown body %q", sample.Body)
}
if seen[sample.Body] {
t.Fatalf("duplicate body %q in apparent baseline", sample.Body)
}
seen[sample.Body] = true
date, err := time.Parse(time.RFC3339, sample.InputUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.InputUTC, err)
}
jd := TD2UT(Date2JDE(date.UTC()), true)
prefix := sample.Body + "." + sample.InputUTC
assertPlanetApparentAngleClose(t, prefix+".RightAscension", tc.ra(jd), sample.RightAscension, 0.001)
assertPlanetPhaseClose(t, prefix+".Declination", tc.dec(jd), sample.Declination, 0.001)
assertPlanetApparentAngleClose(t, prefix+".EclipticLongitude", tc.lo(jd), sample.EclipticLongitude, 0.001)
assertPlanetPhaseClose(t, prefix+".EclipticLatitude", tc.bo(jd), sample.EclipticLatitude, 0.001)
}
for body := range cases {
if !seen[body] {
t.Fatalf("missing body %q in apparent baseline", body)
}
}
}
func assertPlanetApparentAngleClose(t *testing.T, name string, got, want, tolerance float64) {
t.Helper()
if diff := angleDiffAbs(got, want); diff > tolerance {
t.Fatalf("%s mismatch: got %.12f want %.12f diff %.12f tolerance %.12f", name, got, want, diff, tolerance)
}
}
+520
View File
@@ -0,0 +1,520 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
const (
planetTransitMeanSolarMotionDegPerDay = 360.0 / 365.2422
planetTransitTropicalYearDays = 365.2422
planetTransitSeasonProbeStepDays = 20.0
planetTransitSearchLimit = 2400
planetTransitSearchEpsilonDays = 1.0 / 86400.0
planetTransitGreatestWindowDays = 1.2
planetTransitGreatestToleranceDays = 0.25 / 86400.0
planetTransitContactStepDays = 0.02
planetTransitContactSpanDays = 1.0
planetTransitContactToleranceDays = 0.25 / 86400.0
planetTransitCoarseN = 16
)
// PlanetTransitResult 表示一次地心行星凌日结果。
//
// Valid 为 false 时表示没有找到有效凌日。所有时刻字段均为 UT 儒略日。
// MinimumSeparationArcsec、SunSemidiameterArcsec、PlanetSemidiameterArcsec 的单位均为角秒。
type PlanetTransitResult struct {
Valid bool
// PlanetIndex 为行星序号,1 表示水星,2 表示金星。
PlanetIndex int
// ExternalIngress / ExternalEgress 为一触 / 四触。
ExternalIngress float64
ExternalEgress float64
// InternalIngress / InternalEgress 为二触 / 三触。掠凌没有内切接触时为 0。
InternalIngress float64
InternalEgress float64
// Greatest 为凌甚,即行星中心最接近太阳中心的时刻。
Greatest float64
MinimumSeparationArcsec float64
SunSemidiameterArcsec float64
PlanetSemidiameterArcsec float64
HasExternal bool
HasInternal bool
}
type planetTransitConfig struct {
planetIndex int
synodicPeriodDays float64
anchorInferiorTT float64
seasonWindowDays float64
latitudePrefilter float64
conjunctionStepDay float64
apparentLoN func(float64, int) float64
apparentBoN func(float64, int) float64
apparentRaDecN func(float64, int) (float64, float64)
semidiameterN func(float64, int) float64
earthDistanceN func(float64, int) float64
nodeN func(float64, int) float64
}
type planetTransitState struct {
jdTT float64
separationArcsec float64
separationSquared float64
sunSemidiameter float64
planetSemidiameter float64
externalContactMetric float64
internalContactMetric float64
}
func mercuryTransitConfig() planetTransitConfig {
return planetTransitConfig{
planetIndex: 1,
synodicPeriodDays: MERCURY_S_PERIOD,
anchorInferiorTT: TD2UT(JDECalc(2019, 11, 11+(15+21.0/60+40.0/3600)/24), true),
seasonWindowDays: 12,
latitudePrefilter: 1.0,
conjunctionStepDay: 0.00001,
apparentLoN: MercuryApparentLoN,
apparentBoN: MercuryApparentBoN,
apparentRaDecN: MercuryApparentRaDecN,
semidiameterN: MercurySemidiameterN,
earthDistanceN: EarthMercuryAwayN,
nodeN: MercuryAscendingNodeN,
}
}
func venusTransitConfig() planetTransitConfig {
return planetTransitConfig{
planetIndex: 2,
synodicPeriodDays: VENUS_S_PERIOD,
anchorInferiorTT: TD2UT(JDECalc(2012, 6, 6+(1+29.0/60)/24), true),
seasonWindowDays: 8,
latitudePrefilter: 0.8,
conjunctionStepDay: 0.00001,
apparentLoN: VenusApparentLoN,
apparentBoN: VenusApparentBoN,
apparentRaDecN: VenusApparentRaDecN,
semidiameterN: VenusSemidiameterN,
earthDistanceN: EarthVenusAwayN,
nodeN: VenusAscendingNodeN,
}
}
// NextMercuryTransit 返回给定时刻之后的下一次地心水星凌日。
func NextMercuryTransit(jde float64) PlanetTransitResult {
result, _ := searchPlanetTransit(jde, mercuryTransitConfig(), 1, false)
return result
}
// LastMercuryTransit 返回给定时刻之前的上一次地心水星凌日。
func LastMercuryTransit(jde float64) PlanetTransitResult {
result, _ := searchPlanetTransit(jde, mercuryTransitConfig(), -1, true)
return result
}
// ClosestMercuryTransit 返回距给定时刻最近的一次地心水星凌日。
func ClosestMercuryTransit(jde float64) PlanetTransitResult {
return closestPlanetTransit(jde, mercuryTransitConfig())
}
// NextVenusTransit 返回给定时刻之后的下一次地心金星凌日。
func NextVenusTransit(jde float64) PlanetTransitResult {
result, _ := searchPlanetTransit(jde, venusTransitConfig(), 1, false)
return result
}
// LastVenusTransit 返回给定时刻之前的上一次地心金星凌日。
func LastVenusTransit(jde float64) PlanetTransitResult {
result, _ := searchPlanetTransit(jde, venusTransitConfig(), -1, true)
return result
}
// ClosestVenusTransit 返回距给定时刻最近的一次地心金星凌日。
func ClosestVenusTransit(jde float64) PlanetTransitResult {
return closestPlanetTransit(jde, venusTransitConfig())
}
func closestPlanetTransit(jde float64, cfg planetTransitConfig) PlanetTransitResult {
last, hasLast := searchPlanetTransit(jde, cfg, -1, true)
next, hasNext := searchPlanetTransit(jde, cfg, 1, false)
switch {
case hasLast && !hasNext:
return last
case !hasLast && hasNext:
return next
case !hasLast && !hasNext:
return PlanetTransitResult{}
}
if math.Abs(last.Greatest-jde) <= math.Abs(next.Greatest-jde) {
return last
}
return next
}
func searchPlanetTransit(jde float64, cfg planetTransitConfig, direction int, includeCurrent bool) (PlanetTransitResult, bool) {
if !isFiniteFloat(jde) || direction == 0 {
return PlanetTransitResult{}, false
}
targetTT := TD2UT(jde, true)
probeTT := targetTT
for i := 0; i < planetTransitSearchLimit; i++ {
seasonTT, ok := nextPlanetTransitSeasonTT(probeTT, cfg, direction)
if !ok {
return PlanetTransitResult{}, false
}
seedTT := nearestPlanetTransitInferiorSeedTT(seasonTT, cfg)
if math.Abs(seedTT-seasonTT) <= cfg.seasonWindowDays {
conjunctionTT := refinePlanetTransitInferiorConjunctionTT(seedTT, cfg)
if math.Abs(conjunctionTT-seasonTT) <= cfg.seasonWindowDays+1 && isPotentialPlanetTransit(conjunctionTT, cfg) {
resultTT, ok := planetTransitAtInferiorConjunctionTT(conjunctionTT, cfg)
if ok && planetTransitMatchesDirection(resultTT.Greatest, targetTT, direction, includeCurrent) {
return planetTransitResultTTToUT(resultTT), true
}
}
}
probeTT = seasonTT + float64(direction)*planetTransitSeasonProbeStepDays
}
return PlanetTransitResult{}, false
}
func nextPlanetTransitSeasonTT(jdTT float64, cfg planetTransitConfig, direction int) (float64, bool) {
best := math.NaN()
for nodeOffset := 0; nodeOffset <= 1; nodeOffset++ {
candidate := estimatePlanetTransitSeasonTT(jdTT, cfg, nodeOffset, direction)
candidate = refinePlanetTransitSeasonTT(candidate, cfg, nodeOffset)
for !planetTransitMatchesDirection(candidate, jdTT, direction, false) {
candidate += float64(direction) * planetTransitTropicalYearDays
candidate = refinePlanetTransitSeasonTT(candidate, cfg, nodeOffset)
}
if !isFiniteFloat(best) || math.Abs(candidate-jdTT) < math.Abs(best-jdTT) {
best = candidate
}
}
if !isFiniteFloat(best) {
return 0, false
}
return best, true
}
func estimatePlanetTransitSeasonTT(jdTT float64, cfg planetTransitConfig, nodeOffset int, direction int) float64 {
sunLongitude := HSunApparentLoN(jdTT, planetTransitCoarseN)
nodeLongitude := planetTransitNodeLongitude(jdTT, cfg, nodeOffset, planetTransitCoarseN)
if direction > 0 {
delta := Limit360(nodeLongitude - sunLongitude)
if delta <= planetTransitSearchEpsilonDays {
delta += 360
}
return jdTT + delta/planetTransitMeanSolarMotionDegPerDay
}
delta := Limit360(sunLongitude - nodeLongitude)
if delta <= planetTransitSearchEpsilonDays {
delta += 360
}
return jdTT - delta/planetTransitMeanSolarMotionDegPerDay
}
func refinePlanetTransitSeasonTT(seedTT float64, cfg planetTransitConfig, nodeOffset int) float64 {
current := seedTT
for i := 0; i < 8; i++ {
prev := current
value := planetTransitSunNodeLongitudeDelta(prev, cfg, nodeOffset)
slope := (planetTransitSunNodeLongitudeDelta(prev+0.5, cfg, nodeOffset) -
planetTransitSunNodeLongitudeDelta(prev-0.5, cfg, nodeOffset)) / 1.0
if slope == 0 || !isFiniteFloat(slope) {
break
}
current = prev - value/slope
if math.Abs(current-prev) <= 0.00001 {
break
}
}
return current
}
func planetTransitSunNodeLongitudeDelta(jdTT float64, cfg planetTransitConfig, nodeOffset int) float64 {
return planetTransitAngleDelta(HSunApparentLoN(jdTT, planetTransitCoarseN) -
planetTransitNodeLongitude(jdTT, cfg, nodeOffset, planetTransitCoarseN))
}
func planetTransitNodeLongitude(jdTT float64, cfg planetTransitConfig, nodeOffset int, n int) float64 {
return Limit360(cfg.nodeN(jdTT, n) + float64(nodeOffset)*180)
}
func nearestPlanetTransitInferiorSeedTT(seasonTT float64, cfg planetTransitConfig) float64 {
k := math.Round((seasonTT - cfg.anchorInferiorTT) / cfg.synodicPeriodDays)
return cfg.anchorInferiorTT + k*cfg.synodicPeriodDays
}
func refinePlanetTransitInferiorConjunctionTT(seedTT float64, cfg planetTransitConfig) float64 {
current := seedTT
for i := 0; i < 4; i++ {
prev := current
value := planetTransitLongitudeDeltaN(prev, cfg, planetTransitCoarseN)
slope := (planetTransitLongitudeDeltaN(prev+cfg.conjunctionStepDay, cfg, planetTransitCoarseN) -
planetTransitLongitudeDeltaN(prev-cfg.conjunctionStepDay, cfg, planetTransitCoarseN)) / (2 * cfg.conjunctionStepDay)
if slope == 0 || !isFiniteFloat(slope) {
break
}
current = prev - value/slope
if math.Abs(current-prev) <= 30.0/86400.0 {
break
}
}
for i := 0; i < 8; i++ {
prev := current
value := planetTransitLongitudeDeltaN(prev, cfg, -1)
slope := (planetTransitLongitudeDeltaN(prev+cfg.conjunctionStepDay, cfg, -1) -
planetTransitLongitudeDeltaN(prev-cfg.conjunctionStepDay, cfg, -1)) / (2 * cfg.conjunctionStepDay)
if slope == 0 || !isFiniteFloat(slope) {
break
}
current = prev - value/slope
if math.Abs(current-prev) <= cfg.conjunctionStepDay {
break
}
}
return current
}
func planetTransitLongitudeDeltaN(jdTT float64, cfg planetTransitConfig, n int) float64 {
return planetTransitAngleDelta(cfg.apparentLoN(jdTT, n) - HSunApparentLoN(jdTT, n))
}
func isPotentialPlanetTransit(conjunctionTT float64, cfg planetTransitConfig) bool {
if cfg.earthDistanceN(conjunctionTT, planetTransitCoarseN) > EarthAwayN(conjunctionTT, planetTransitCoarseN) {
return false
}
return math.Abs(cfg.apparentBoN(conjunctionTT, planetTransitCoarseN)) <= cfg.latitudePrefilter
}
func planetTransitAtInferiorConjunctionTT(conjunctionTT float64, cfg planetTransitConfig) (PlanetTransitResult, bool) {
greatestTT := greatestPlanetTransitTT(conjunctionTT, cfg)
greatestState := planetTransitStateAt(greatestTT, cfg, -1)
if !isFiniteFloat(greatestState.externalContactMetric) || greatestState.externalContactMetric > 0 {
return PlanetTransitResult{}, false
}
result := PlanetTransitResult{
Valid: true,
PlanetIndex: cfg.planetIndex,
Greatest: greatestTT,
MinimumSeparationArcsec: greatestState.separationArcsec,
SunSemidiameterArcsec: greatestState.sunSemidiameter,
PlanetSemidiameterArcsec: greatestState.planetSemidiameter,
HasExternal: true,
HasInternal: greatestState.internalContactMetric <= 0,
}
externalIngress, ok := refinePlanetTransitContactTT(greatestTT, cfg, -1, false)
if !ok {
return PlanetTransitResult{}, false
}
externalEgress, ok := refinePlanetTransitContactTT(greatestTT, cfg, 1, false)
if !ok || externalEgress <= externalIngress {
return PlanetTransitResult{}, false
}
result.ExternalIngress = externalIngress
result.ExternalEgress = externalEgress
if result.HasInternal {
internalIngress, ok := refinePlanetTransitContactTT(greatestTT, cfg, -1, true)
if ok {
result.InternalIngress = internalIngress
}
internalEgress, ok := refinePlanetTransitContactTT(greatestTT, cfg, 1, true)
if ok && internalEgress > internalIngress {
result.InternalEgress = internalEgress
}
result.HasInternal = result.InternalIngress != 0 && result.InternalEgress != 0
}
return result, true
}
func greatestPlanetTransitTT(seedTT float64, cfg planetTransitConfig) float64 {
left := seedTT - planetTransitGreatestWindowDays
right := seedTT + planetTransitGreatestWindowDays
goldenRatio := (math.Sqrt(5) - 1) / 2
x1 := right - goldenRatio*(right-left)
x2 := left + goldenRatio*(right-left)
f1 := planetTransitStateAt(x1, cfg, planetTransitCoarseN).separationSquared
f2 := planetTransitStateAt(x2, cfg, planetTransitCoarseN).separationSquared
for i := 0; i < 80 && right-left > planetTransitGreatestToleranceDays; i++ {
if f1 <= f2 {
right = x2
x2 = x1
f2 = f1
x1 = right - goldenRatio*(right-left)
f1 = planetTransitStateAt(x1, cfg, planetTransitCoarseN).separationSquared
continue
}
left = x1
x1 = x2
f1 = f2
x2 = left + goldenRatio*(right-left)
f2 = planetTransitStateAt(x2, cfg, planetTransitCoarseN).separationSquared
}
center := (left + right) / 2
left = center - 2.0/24.0
right = center + 2.0/24.0
x1 = right - goldenRatio*(right-left)
x2 = left + goldenRatio*(right-left)
f1 = planetTransitStateAt(x1, cfg, -1).separationSquared
f2 = planetTransitStateAt(x2, cfg, -1).separationSquared
for i := 0; i < 80 && right-left > planetTransitGreatestToleranceDays; i++ {
if f1 <= f2 {
right = x2
x2 = x1
f2 = f1
x1 = right - goldenRatio*(right-left)
f1 = planetTransitStateAt(x1, cfg, -1).separationSquared
continue
}
left = x1
x1 = x2
f1 = f2
x2 = left + goldenRatio*(right-left)
f2 = planetTransitStateAt(x2, cfg, -1).separationSquared
}
return (left + right) / 2
}
func planetTransitStateAt(jdTT float64, cfg planetTransitConfig, n int) planetTransitState {
planetRA, planetDec := cfg.apparentRaDecN(jdTT, n)
sunRA, sunDec := HSunApparentRaDecN(jdTT, n)
separationArcsec := StarAngularSeparation(planetRA, planetDec, sunRA, sunDec) * 3600
sunSemidiameter := SunSemidiameterN(jdTT, n)
planetSemidiameter := cfg.semidiameterN(jdTT, n)
return planetTransitState{
jdTT: jdTT,
separationArcsec: separationArcsec,
separationSquared: separationArcsec * separationArcsec,
sunSemidiameter: sunSemidiameter,
planetSemidiameter: planetSemidiameter,
externalContactMetric: separationArcsec - (sunSemidiameter + planetSemidiameter),
internalContactMetric: separationArcsec - (sunSemidiameter - planetSemidiameter),
}
}
func refinePlanetTransitContactTT(greatestTT float64, cfg planetTransitConfig, direction int, internal bool) (float64, bool) {
if direction != -1 && direction != 1 {
return 0, false
}
metric := func(jdTT float64) float64 {
state := planetTransitStateAt(jdTT, cfg, -1)
if internal {
return state.internalContactMetric
}
return state.externalContactMetric
}
nearJD := greatestTT
nearValue := metric(nearJD)
if !isFiniteFloat(nearValue) || nearValue > 0 {
return 0, false
}
maxSteps := int(math.Ceil(planetTransitContactSpanDays / planetTransitContactStepDays))
for i := 1; i <= maxSteps; i++ {
farJD := greatestTT + float64(direction)*planetTransitContactStepDays*float64(i)
farValue := metric(farJD)
if !isFiniteFloat(farValue) {
continue
}
if farValue >= 0 {
return bisectPlanetTransitContactTT(nearJD, nearValue, farJD, farValue, metric)
}
nearJD = farJD
nearValue = farValue
}
return 0, false
}
func bisectPlanetTransitContactTT(leftJD, leftValue, rightJD, rightValue float64, metric func(float64) float64) (float64, bool) {
if leftJD > rightJD {
leftJD, rightJD = rightJD, leftJD
leftValue, rightValue = rightValue, leftValue
}
if leftValue == 0 {
return leftJD, true
}
if rightValue == 0 {
return rightJD, true
}
if leftValue*rightValue > 0 {
return 0, false
}
for i := 0; i < 80 && rightJD-leftJD > planetTransitContactToleranceDays; i++ {
midJD := (leftJD + rightJD) / 2
midValue := metric(midJD)
if !isFiniteFloat(midValue) {
return 0, false
}
if midValue == 0 {
return midJD, true
}
if leftValue*midValue <= 0 {
rightJD = midJD
rightValue = midValue
continue
}
leftJD = midJD
leftValue = midValue
}
return (leftJD + rightJD) / 2, true
}
func planetTransitResultTTToUT(result PlanetTransitResult) PlanetTransitResult {
result.Greatest = TD2UT(result.Greatest, false)
result.ExternalIngress = TD2UT(result.ExternalIngress, false)
result.ExternalEgress = TD2UT(result.ExternalEgress, false)
if result.InternalIngress != 0 {
result.InternalIngress = TD2UT(result.InternalIngress, false)
}
if result.InternalEgress != 0 {
result.InternalEgress = TD2UT(result.InternalEgress, false)
}
return result
}
func planetTransitMatchesDirection(eventJDE, targetJDE float64, direction int, includeCurrent bool) bool {
delta := eventJDE - targetJDE
if math.Abs(delta) <= planetTransitSearchEpsilonDays {
return includeCurrent
}
if direction > 0 {
return delta > 0
}
return delta < 0
}
func planetTransitAngleDelta(diff float64) float64 {
diff = Limit360(diff)
if diff > 180 {
diff -= 360
}
if diff < -180 {
diff += 360
}
return diff
}
func isFiniteFloat(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
+145
View File
@@ -0,0 +1,145 @@
package basic
import (
"math"
"testing"
"time"
)
func TestKnownMercuryTransits(t *testing.T) {
tests := []struct {
name string
query time.Time
greatest time.Time
}{
{
name: "2016 May",
query: time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC),
greatest: time.Date(2016, 5, 9, 14, 57, 0, 0, time.UTC),
},
{
name: "2019 Nov",
query: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC),
greatest: time.Date(2019, 11, 11, 15, 20, 0, 0, time.UTC),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := NextMercuryTransit(Date2JDE(tc.query))
if !result.Valid {
t.Fatal("expected valid transit")
}
got := JDE2DateByZone(result.Greatest, time.UTC, false)
t.Logf("start=%s greatest=%s end=%s min=%.3f sun=%.3f planet=%.3f",
JDE2DateByZone(result.ExternalIngress, time.UTC, false),
got,
JDE2DateByZone(result.ExternalEgress, time.UTC, false),
result.MinimumSeparationArcsec,
result.SunSemidiameterArcsec,
result.PlanetSemidiameterArcsec,
)
if math.Abs(got.Sub(tc.greatest).Seconds()) > 20*60 {
t.Fatalf("greatest mismatch: got %s want near %s", got, tc.greatest)
}
if !result.HasInternal {
t.Fatalf("expected internal contacts")
}
if !(result.ExternalIngress < result.InternalIngress &&
result.InternalIngress < result.Greatest &&
result.Greatest < result.InternalEgress &&
result.InternalEgress < result.ExternalEgress) {
t.Fatalf("contacts out of order: %+v", result)
}
})
}
}
func TestKnownVenusTransits(t *testing.T) {
tests := []struct {
name string
query time.Time
greatest time.Time
}{
{
name: "2004 Jun",
query: time.Date(2004, 1, 1, 0, 0, 0, 0, time.UTC),
greatest: time.Date(2004, 6, 8, 8, 20, 0, 0, time.UTC),
},
{
name: "2012 Jun",
query: time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC),
greatest: time.Date(2012, 6, 6, 1, 29, 0, 0, time.UTC),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := NextVenusTransit(Date2JDE(tc.query))
if !result.Valid {
t.Fatal("expected valid transit")
}
got := JDE2DateByZone(result.Greatest, time.UTC, false)
t.Logf("start=%s greatest=%s end=%s min=%.3f sun=%.3f planet=%.3f",
JDE2DateByZone(result.ExternalIngress, time.UTC, false),
got,
JDE2DateByZone(result.ExternalEgress, time.UTC, false),
result.MinimumSeparationArcsec,
result.SunSemidiameterArcsec,
result.PlanetSemidiameterArcsec,
)
if math.Abs(got.Sub(tc.greatest).Seconds()) > 20*60 {
t.Fatalf("greatest mismatch: got %s want near %s", got, tc.greatest)
}
if !result.HasInternal {
t.Fatalf("expected internal contacts")
}
if !(result.ExternalIngress < result.InternalIngress &&
result.InternalIngress < result.Greatest &&
result.Greatest < result.InternalEgress &&
result.InternalEgress < result.ExternalEgress) {
t.Fatalf("contacts out of order: %+v", result)
}
})
}
}
func TestTransitSearchSkipsSparseEvents(t *testing.T) {
mercuryResult := NextMercuryTransit(Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)))
if !mercuryResult.Valid {
t.Fatal("expected Mercury transit")
}
mercuryGreatest := JDE2DateByZone(mercuryResult.Greatest, time.UTC, false)
if mercuryGreatest.Year() != 2032 || mercuryGreatest.Month() != time.November {
t.Fatalf("unexpected next Mercury transit: %s", mercuryGreatest)
}
venusResult := NextVenusTransit(Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)))
if !venusResult.Valid {
t.Fatal("expected Venus transit")
}
venusGreatest := JDE2DateByZone(venusResult.Greatest, time.UTC, false)
if venusGreatest.Year() != 2117 || venusGreatest.Month() != time.December {
t.Fatalf("unexpected next Venus transit: %s", venusGreatest)
}
}
func BenchmarkNextMercuryTransitFrom2026(b *testing.B) {
jd := Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
for i := 0; i < b.N; i++ {
result := NextMercuryTransit(jd)
if !result.Valid {
b.Fatal("expected valid transit")
}
}
}
func BenchmarkNextVenusTransitFrom2026(b *testing.B) {
jd := Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
for i := 0; i < b.N; i++ {
result := NextVenusTransit(jd)
if !result.Valid {
b.Fatal("expected valid transit")
}
}
}
+3 -10
View File
@@ -24,14 +24,8 @@ func planetXYZN(planetIndex int, jd float64, n int) (float64, float64, float64)
}
func planetApparentLoBoN(planetIndex int, jd float64, n int) (float64, float64) {
x, y, z := planetXYZN(planetIndex, jd, n)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = planetXYZN(planetIndex, jd-to, n)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = Limit360(lo*180/math.Pi) + Nutation2000Bi(jd)
bo = bo * 180 / math.Pi
return lo, bo
geo, _ := planetApparentGeocentricPositionN(planetIndex, jd, n)
return geo.lo, geo.bo
}
func planetApparentRaManualN(planetIndex int, jd float64, n int) float64 {
@@ -57,8 +51,7 @@ func planetApparentRaDecManualN(planetIndex int, jd float64, n int) (float64, fl
}
func planetEarthAwayN(planetIndex int, jd float64, n int) float64 {
x, y, z := planetXYZN(planetIndex, jd, n)
return math.Sqrt(x*x + y*y + z*z)
return planetEarthAwayExplicitN(planetIndex, jd, n)
}
func planetHeightN(jde, lon, lat, timezone float64, n int, apparentRaDec func(float64, int) (float64, float64)) float64 {
+7 -38
View File
@@ -87,53 +87,22 @@ func SaturnApparentRaDec(jd float64) (float64, float64) {
}
func EarthSaturnAway(jd float64) float64 {
x, y, z := ASaturnXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(5, jd, -1)
}
func SaturnApparentLo(jd float64) float64 {
x, y, z := ASaturnXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ASaturnXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(5, jd, -1)
return geo.lo
}
func SaturnApparentBo(jd float64) float64 {
x, y, z := ASaturnXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ASaturnXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(5, jd, -1)
return geo.bo
}
func SaturnApparentLoBo(jd float64) (float64, float64) {
x, y, z := ASaturnXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ASaturnXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo, bo
geo, _ := planetApparentGeocentricPositionN(5, jd, -1)
return geo.lo, geo.bo
}
func SaturnMag(jd float64) float64 {
+66 -65
View File
@@ -40,6 +40,28 @@ func saturnSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64 {
return sub
}
func saturnRADerivative(jde, delta float64) float64 {
sub := SaturnApparentRa(jde+delta) - SaturnApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func saturnRADerivativeN(jde, delta float64, n int) float64 {
sub := SaturnApparentRaN(jde+delta, n) - SaturnApparentRaN(jde-delta, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func saturnConjunctionFull(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := SATURN_S_PERIOD / 360
@@ -94,113 +116,92 @@ func saturnConjunction(jde, degree float64, next uint8) float64 {
}
func LastSaturnConjunction(jde float64) float64 {
return saturnConjunction(jde, 0, 0)
return inclusiveLastPhaseEvent(jde, 0, saturnConjunction)
}
func NextSaturnConjunction(jde float64) float64 {
return saturnConjunction(jde, 0, 1)
return inclusiveNextPhaseEvent(jde, 0, saturnConjunction)
}
func LastSaturnOpposition(jde float64) float64 {
return saturnConjunction(jde, 180, 0)
return inclusiveLastPhaseEvent(jde, 180, saturnConjunction)
}
func NextSaturnOpposition(jde float64) float64 {
return saturnConjunction(jde, 180, 1)
return inclusiveNextPhaseEvent(jde, 180, saturnConjunction)
}
func NextSaturnEasternQuadrature(jde float64) float64 {
return saturnConjunction(jde, 90, 1)
return inclusiveNextPhaseEvent(jde, 90, saturnConjunction)
}
func LastSaturnEasternQuadrature(jde float64) float64 {
return saturnConjunction(jde, 90, 0)
return inclusiveLastPhaseEvent(jde, 90, saturnConjunction)
}
func NextSaturnWesternQuadrature(jde float64) float64 {
return saturnConjunction(jde, 270, 1)
return inclusiveNextPhaseEvent(jde, 270, saturnConjunction)
}
func LastSaturnWesternQuadrature(jde float64) float64 {
return saturnConjunction(jde, 270, 0)
return inclusiveLastPhaseEvent(jde, 270, saturnConjunction)
}
func saturnRetrograde(jde float64, searchBeforeOpposition bool) float64 {
//0=last 1=next
raRate := func(jde float64, delta float64) float64 {
sub := SaturnApparentRa(jde+delta) - SaturnApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
jde = saturnConjunctionFull(jde, 180, 1)
func saturnRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
oppositionTT := TD2UT(oppositionJD, true)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
jde -= 60
easternQuadratureUT := saturnConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
} else {
jde += 60
westernQuadratureUT := saturnConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true)
}
for {
currentRate := raRate(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
rateValue := raRate(prevJD, 2.0/86400.0)
rateSlope := (raRate(prevJD+15.0/86400.0, 2.0/86400.0) - raRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return raRate(jd, 0.5/86400.0)
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return saturnRADerivativeN(jd, 1.0/86400.0, saturnEventSearchN)
}, func(jd float64) float64 {
return saturnRADerivative(jd, 0.5/86400.0)
})
return TD2UT(bestJD, false)
}
func NextSaturnRetrogradeToPrograde(jde float64) float64 {
date := saturnRetrograde(jde, false)
if date < jde {
oppositionJD := saturnConjunctionFull(jde, 180, 1)
return saturnRetrograde(oppositionJD+10, false)
}
lastOppositionJD := saturnConjunctionFull(jde, 180, 0)
date := saturnRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextOppositionJD := saturnConjunctionFull(jde, 180, 1)
return saturnRetrogradeAroundOpposition(nextOppositionJD, false)
}
func LastSaturnRetrogradeToPrograde(jde float64) float64 {
jde = saturnConjunctionFull(jde, 180, 0) - 10
date := saturnRetrograde(jde, false)
if date > jde {
oppositionJD := saturnConjunctionFull(jde, 180, 0)
return saturnRetrograde(oppositionJD-10, false)
}
lastOppositionJD := saturnConjunctionFull(jde, 180, 0)
date := saturnRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
previousOppositionJD := saturnConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return saturnRetrogradeAroundOpposition(previousOppositionJD, false)
}
func NextSaturnProgradeToRetrograde(jde float64) float64 {
date := saturnRetrograde(jde, true)
if date < jde {
oppositionJD := saturnConjunctionFull(jde, 180, 1)
return saturnRetrograde(oppositionJD+10, true)
}
nextOppositionJD := saturnConjunctionFull(jde, 180, 1)
date := saturnRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
followingOppositionJD := saturnConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return saturnRetrogradeAroundOpposition(followingOppositionJD, true)
}
func LastSaturnProgradeToRetrograde(jde float64) float64 {
jde = saturnConjunctionFull(jde, 180, 0) - 10
date := saturnRetrograde(jde, true)
if date > jde {
oppositionJD := saturnConjunctionFull(jde, 180, 0)
return saturnRetrograde(oppositionJD-10, true)
}
nextOppositionJD := saturnConjunctionFull(jde, 180, 1)
date := saturnRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
lastOppositionJD := saturnConjunctionFull(jde, 180, 0)
return saturnRetrogradeAroundOpposition(lastOppositionJD, true)
}
+207
View File
@@ -0,0 +1,207 @@
package basic
import (
"testing"
"time"
)
type stationEvent struct {
when time.Time
kind string
}
type stationTruthCase struct {
name string
events []stationEvent
lastR2P func(float64) float64
nextR2P func(float64) float64
lastP2R func(float64) float64
nextP2R func(float64) float64
}
func mustJST(value string) time.Time {
loc := time.FixedZone("JST", 9*3600)
t, err := time.ParseInLocation("2006-01-02 15:04", value, loc)
if err != nil {
panic(err)
}
return t
}
func toUTJD(t time.Time) float64 {
return TD2UT(Date2JDE(t.UTC()), true)
}
func TestStationTruthAgainstNAOJ(t *testing.T) {
cases := []stationTruthCase{
{
name: "Mars",
events: []stationEvent{
{when: mustJST("2024-12-08 05:59"), kind: "P2R"},
{when: mustJST("2025-01-16 11:39"), kind: "OPP"},
{when: mustJST("2025-02-24 18:35"), kind: "R2P"},
{when: mustJST("2027-01-12 01:10"), kind: "P2R"},
{when: mustJST("2027-02-20 00:51"), kind: "OPP"},
{when: mustJST("2027-04-03 02:33"), kind: "R2P"},
},
lastR2P: LastMarsRetrogradeToPrograde,
nextR2P: NextMarsRetrogradeToPrograde,
lastP2R: LastMarsProgradeToRetrograde,
nextP2R: NextMarsProgradeToRetrograde,
},
{
name: "Jupiter",
events: []stationEvent{
{when: mustJST("2024-10-09 16:13"), kind: "P2R"},
{when: mustJST("2024-12-08 05:58"), kind: "OPP"},
{when: mustJST("2025-02-04 22:07"), kind: "R2P"},
{when: mustJST("2025-11-12 04:54"), kind: "P2R"},
{when: mustJST("2026-01-10 17:42"), kind: "OPP"},
{when: mustJST("2026-03-11 11:44"), kind: "R2P"},
{when: mustJST("2026-12-13 21:03"), kind: "P2R"},
{when: mustJST("2027-02-11 09:29"), kind: "OPP"},
{when: mustJST("2027-04-13 15:17"), kind: "R2P"},
},
lastR2P: LastJupiterRetrogradeToPrograde,
nextR2P: NextJupiterRetrogradeToPrograde,
lastP2R: LastJupiterProgradeToRetrograde,
nextP2R: NextJupiterProgradeToRetrograde,
},
{
name: "Saturn",
events: []stationEvent{
{when: mustJST("2024-07-01 06:15"), kind: "P2R"},
{when: mustJST("2024-09-08 13:35"), kind: "OPP"},
{when: mustJST("2024-11-16 14:57"), kind: "R2P"},
{when: mustJST("2025-07-14 16:57"), kind: "P2R"},
{when: mustJST("2025-09-21 14:46"), kind: "OPP"},
{when: mustJST("2025-11-29 09:35"), kind: "R2P"},
{when: mustJST("2026-07-28 08:09"), kind: "P2R"},
{when: mustJST("2026-10-04 21:29"), kind: "OPP"},
{when: mustJST("2026-12-12 08:21"), kind: "R2P"},
{when: mustJST("2027-08-11 02:53"), kind: "P2R"},
{when: mustJST("2027-10-18 09:36"), kind: "OPP"},
{when: mustJST("2027-12-25 12:05"), kind: "R2P"},
},
lastR2P: LastSaturnRetrogradeToPrograde,
nextR2P: NextSaturnRetrogradeToPrograde,
lastP2R: LastSaturnProgradeToRetrograde,
nextP2R: NextSaturnProgradeToRetrograde,
},
{
name: "Uranus",
events: []stationEvent{
{when: mustJST("2024-01-27 19:50"), kind: "R2P"},
{when: mustJST("2024-09-02 00:44"), kind: "P2R"},
{when: mustJST("2024-11-17 11:45"), kind: "OPP"},
{when: mustJST("2025-01-31 04:04"), kind: "R2P"},
{when: mustJST("2025-09-06 13:55"), kind: "P2R"},
{when: mustJST("2025-11-21 21:25"), kind: "OPP"},
{when: mustJST("2026-02-04 13:37"), kind: "R2P"},
{when: mustJST("2026-09-11 03:19"), kind: "P2R"},
{when: mustJST("2026-11-26 07:41"), kind: "OPP"},
{when: mustJST("2027-02-08 23:03"), kind: "R2P"},
{when: mustJST("2027-09-15 17:50"), kind: "P2R"},
{when: mustJST("2027-11-30 18:22"), kind: "OPP"},
},
lastR2P: LastUranusRetrogradeToPrograde,
nextR2P: NextUranusRetrogradeToPrograde,
lastP2R: LastUranusProgradeToRetrograde,
nextP2R: NextUranusProgradeToRetrograde,
},
{
name: "Neptune",
events: []stationEvent{
{when: mustJST("2024-07-03 12:08"), kind: "P2R"},
{when: mustJST("2024-09-21 09:17"), kind: "OPP"},
{when: mustJST("2024-12-08 20:05"), kind: "R2P"},
{when: mustJST("2025-07-05 23:30"), kind: "P2R"},
{when: mustJST("2025-09-23 21:54"), kind: "OPP"},
{when: mustJST("2025-12-11 09:21"), kind: "R2P"},
{when: mustJST("2026-07-08 13:02"), kind: "P2R"},
{when: mustJST("2026-09-26 10:36"), kind: "OPP"},
{when: mustJST("2026-12-13 19:47"), kind: "R2P"},
{when: mustJST("2027-07-11 01:06"), kind: "P2R"},
{when: mustJST("2027-09-28 23:19"), kind: "OPP"},
{when: mustJST("2027-12-16 07:16"), kind: "R2P"},
},
lastR2P: LastNeptuneRetrogradeToPrograde,
nextR2P: NextNeptuneRetrogradeToPrograde,
lastP2R: LastNeptuneProgradeToRetrograde,
nextP2R: NextNeptuneProgradeToRetrograde,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
for i, event := range tc.events {
switch event.kind {
case "P2R":
before := event.when.Add(-24 * time.Hour)
after := event.when.Add(24 * time.Hour)
nextP2R := JDE2DateByZone(tc.nextP2R(toUTJD(before)), event.when.Location(), false)
lastP2R := JDE2DateByZone(tc.lastP2R(toUTJD(after)), event.when.Location(), false)
if !sameMinute(nextP2R, event.when) {
t.Fatalf("%s next P2R mismatch: got %s want %s", tc.name, nextP2R, event.when)
}
if !sameMinute(lastP2R, event.when) {
t.Fatalf("%s last P2R mismatch: got %s want %s", tc.name, lastP2R, event.when)
}
case "R2P":
before := event.when.Add(-24 * time.Hour)
after := event.when.Add(24 * time.Hour)
nextR2P := JDE2DateByZone(tc.nextR2P(toUTJD(before)), event.when.Location(), false)
lastR2P := JDE2DateByZone(tc.lastR2P(toUTJD(after)), event.when.Location(), false)
if !sameMinute(nextR2P, event.when) {
t.Fatalf("%s next R2P mismatch: got %s want %s", tc.name, nextR2P, event.when)
}
if !sameMinute(lastR2P, event.when) {
t.Fatalf("%s last R2P mismatch: got %s want %s", tc.name, lastR2P, event.when)
}
case "OPP":
prev := nearestOfKindBefore(tc.events, i, "P2R")
next := nearestOfKindAfter(tc.events, i, "R2P")
if prev.IsZero() || next.IsZero() {
continue
}
query := event.when
lastP2R := JDE2DateByZone(tc.lastP2R(toUTJD(query)), query.Location(), false)
nextR2P := JDE2DateByZone(tc.nextR2P(toUTJD(query)), query.Location(), false)
if !sameMinute(lastP2R, prev) {
t.Fatalf("%s opposition last P2R mismatch: got %s want %s", tc.name, lastP2R, prev)
}
if !sameMinute(nextR2P, next) {
t.Fatalf("%s opposition next R2P mismatch: got %s want %s", tc.name, nextR2P, next)
}
}
}
})
}
}
func nearestOfKindBefore(events []stationEvent, idx int, kind string) time.Time {
for i := idx - 1; i >= 0; i-- {
if events[i].kind == kind {
return events[i].when
}
}
return time.Time{}
}
func nearestOfKindAfter(events []stationEvent, idx int, kind string) time.Time {
for i := idx + 1; i < len(events); i++ {
if events[i].kind == kind {
return events[i].when
}
}
return time.Time{}
}
func sameMinute(got, want time.Time) bool {
diff := got.Sub(want)
if diff < 0 {
diff = -diff
}
return diff <= 2*time.Minute
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1152 -1152
View File
File diff suppressed because it is too large Load Diff
+1722 -1722
View File
File diff suppressed because it is too large Load Diff
+1728 -1728
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
[
{
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 63.920306258,
"declination": 26.403701421,
"ecliptic_longitude": 66.7156363,
"ecliptic_latitude": 5.0490966
}
,
{
"input_utc": "2026-05-01T00:00:00Z",
"right_ascension": 208.849626532,
"declination": -16.256039871,
"ecliptic_longitude": 212.5315932,
"ecliptic_latitude": -4.1622995
}
,
{
"input_utc": "2026-08-29T00:00:00Z",
"right_ascension": 346.062573698,
"declination": -4.416718092,
"ecliptic_longitude": 345.4608613,
"ecliptic_latitude": 1.4247855
}
,
{
"input_utc": "2026-12-27T00:00:00Z",
"right_ascension": 139.195692903,
"declination": 16.272137989,
"ecliptic_longitude": 136.6058459,
"ecliptic_latitude": 0.4338603
}
]
+94
View File
@@ -0,0 +1,94 @@
{
"samples": [
{"planet":"mercury","year":2026,"month":1,"time_utc":"2026-01-18T15:06:38Z"},
{"planet":"mercury","year":2026,"month":2,"time_utc":"2026-02-18T23:02:34Z"},
{"planet":"mercury","year":2026,"month":3,"time_utc":"2026-03-17T14:07:16Z"},
{"planet":"mercury","year":2026,"month":4,"time_utc":"2026-04-15T19:11:09Z"},
{"planet":"mercury","year":2026,"month":5,"time_utc":"2026-05-17T02:50:17Z"},
{"planet":"mercury","year":2026,"month":6,"time_utc":"2026-06-16T19:32:07Z"},
{"planet":"mercury","year":2026,"month":7,"time_utc":"2026-07-14T04:37:03Z"},
{"planet":"mercury","year":2026,"month":8,"time_utc":"2026-08-11T12:47:22Z"},
{"planet":"mercury","year":2026,"month":9,"time_utc":"2026-09-12T07:28:45Z"},
{"planet":"mercury","year":2026,"month":10,"time_utc":"2026-10-12T20:08:25Z"},
{"planet":"mercury","year":2026,"month":11,"time_utc":"2026-11-08T16:33:09Z"},
{"planet":"mercury","year":2026,"month":12,"time_utc":"2026-12-07T22:03:14Z"},
{"planet":"venus","year":2026,"month":1,"time_utc":"2026-01-19T01:01:00Z"},
{"planet":"venus","year":2026,"month":2,"time_utc":"2026-02-18T09:20:04Z"},
{"planet":"venus","year":2026,"month":3,"time_utc":"2026-03-20T12:37:37Z"},
{"planet":"venus","year":2026,"month":4,"time_utc":"2026-04-19T08:47:17Z"},
{"planet":"venus","year":2026,"month":5,"time_utc":"2026-05-19T01:49:30Z"},
{"planet":"venus","year":2026,"month":6,"time_utc":"2026-06-17T20:20:35Z"},
{"planet":"venus","year":2026,"month":7,"time_utc":"2026-07-17T16:31:26Z"},
{"planet":"venus","year":2026,"month":8,"time_utc":"2026-08-16T08:47:10Z"},
{"planet":"venus","year":2026,"month":9,"time_utc":"2026-09-14T11:10:57Z"},
{"planet":"venus","year":2026,"month":10,"time_utc":"2026-10-12T02:31:37Z"},
{"planet":"venus","year":2026,"month":11,"time_utc":"2026-11-07T11:33:28Z"},
{"planet":"venus","year":2026,"month":12,"time_utc":"2026-12-05T10:44:42Z"},
{"planet":"mars","year":2026,"month":1,"time_utc":"2026-01-18T14:09:59Z"},
{"planet":"mars","year":2026,"month":2,"time_utc":"2026-02-16T17:40:45Z"},
{"planet":"mars","year":2026,"month":3,"time_utc":"2026-03-17T21:52:35Z"},
{"planet":"mars","year":2026,"month":4,"time_utc":"2026-04-16T00:46:20Z"},
{"planet":"mars","year":2026,"month":5,"time_utc":"2026-05-15T00:44:20Z"},
{"planet":"mars","year":2026,"month":6,"time_utc":"2026-06-12T21:15:20Z"},
{"planet":"mars","year":2026,"month":7,"time_utc":"2026-07-11T14:38:57Z"},
{"planet":"mars","year":2026,"month":8,"time_utc":"2026-08-09T05:31:55Z"},
{"planet":"mars","year":2026,"month":9,"time_utc":"2026-09-06T18:25:43Z"},
{"planet":"mars","year":2026,"month":10,"time_utc":"2026-10-05T05:31:58Z"},
{"planet":"mars","year":2026,"month":11,"time_utc":"2026-11-02T14:25:02Z"},
{"planet":"mars","year":2026,"month":11,"time_utc":"2026-11-30T19:34:48Z"},
{"planet":"mars","year":2026,"month":12,"time_utc":"2026-12-28T17:44:28Z"},
{"planet":"jupiter","year":2026,"month":1,"time_utc":"2026-01-03T21:59:20Z"},
{"planet":"jupiter","year":2026,"month":1,"time_utc":"2026-01-31T02:29:07Z"},
{"planet":"jupiter","year":2026,"month":2,"time_utc":"2026-02-27T06:24:21Z"},
{"planet":"jupiter","year":2026,"month":3,"time_utc":"2026-03-26T12:11:13Z"},
{"planet":"jupiter","year":2026,"month":4,"time_utc":"2026-04-22T22:03:39Z"},
{"planet":"jupiter","year":2026,"month":5,"time_utc":"2026-05-20T12:36:55Z"},
{"planet":"jupiter","year":2026,"month":6,"time_utc":"2026-06-17T06:51:28Z"},
{"planet":"jupiter","year":2026,"month":7,"time_utc":"2026-07-15T03:03:33Z"},
{"planet":"jupiter","year":2026,"month":8,"time_utc":"2026-08-11T23:22:55Z"},
{"planet":"jupiter","year":2026,"month":9,"time_utc":"2026-09-08T18:11:18Z"},
{"planet":"jupiter","year":2026,"month":10,"time_utc":"2026-10-06T10:16:20Z"},
{"planet":"jupiter","year":2026,"month":11,"time_utc":"2026-11-02T23:09:31Z"},
{"planet":"jupiter","year":2026,"month":11,"time_utc":"2026-11-30T09:16:19Z"},
{"planet":"jupiter","year":2026,"month":12,"time_utc":"2026-12-27T17:30:35Z"},
{"planet":"saturn","year":2026,"month":1,"time_utc":"2026-01-23T12:40:10Z"},
{"planet":"saturn","year":2026,"month":2,"time_utc":"2026-02-20T00:03:21Z"},
{"planet":"saturn","year":2026,"month":3,"time_utc":"2026-03-19T14:12:20Z"},
{"planet":"saturn","year":2026,"month":4,"time_utc":"2026-04-16T06:08:13Z"},
{"planet":"saturn","year":2026,"month":5,"time_utc":"2026-05-13T21:58:07Z"},
{"planet":"saturn","year":2026,"month":6,"time_utc":"2026-06-10T11:41:01Z"},
{"planet":"saturn","year":2026,"month":7,"time_utc":"2026-07-07T21:49:36Z"},
{"planet":"saturn","year":2026,"month":8,"time_utc":"2026-08-04T04:10:47Z"},
{"planet":"saturn","year":2026,"month":8,"time_utc":"2026-08-31T08:07:26Z"},
{"planet":"saturn","year":2026,"month":9,"time_utc":"2026-09-27T12:00:54Z"},
{"planet":"saturn","year":2026,"month":10,"time_utc":"2026-10-24T17:41:49Z"},
{"planet":"saturn","year":2026,"month":11,"time_utc":"2026-11-21T01:26:46Z"},
{"planet":"saturn","year":2026,"month":12,"time_utc":"2026-12-18T10:18:35Z"},
{"planet":"uranus","year":2026,"month":1,"time_utc":"2026-01-27T18:46:51Z"},
{"planet":"uranus","year":2026,"month":2,"time_utc":"2026-02-24T00:35:51Z"},
{"planet":"uranus","year":2026,"month":3,"time_utc":"2026-03-23T07:40:49Z"},
{"planet":"uranus","year":2026,"month":4,"time_utc":"2026-04-19T17:35:45Z"},
{"planet":"uranus","year":2026,"month":5,"time_utc":"2026-05-17T05:58:41Z"},
{"planet":"uranus","year":2026,"month":6,"time_utc":"2026-06-13T19:08:48Z"},
{"planet":"uranus","year":2026,"month":7,"time_utc":"2026-07-11T07:07:53Z"},
{"planet":"uranus","year":2026,"month":8,"time_utc":"2026-08-07T16:30:03Z"},
{"planet":"uranus","year":2026,"month":9,"time_utc":"2026-09-03T23:05:03Z"},
{"planet":"uranus","year":2026,"month":10,"time_utc":"2026-10-01T04:18:41Z"},
{"planet":"uranus","year":2026,"month":10,"time_utc":"2026-10-28T10:24:46Z"},
{"planet":"uranus","year":2026,"month":11,"time_utc":"2026-11-24T18:39:41Z"},
{"planet":"uranus","year":2026,"month":12,"time_utc":"2026-12-22T04:19:54Z"},
{"planet":"neptune","year":2026,"month":1,"time_utc":"2026-01-23T15:49:28Z"},
{"planet":"neptune","year":2026,"month":2,"time_utc":"2026-02-19T23:30:09Z"},
{"planet":"neptune","year":2026,"month":3,"time_utc":"2026-03-19T09:34:27Z"},
{"planet":"neptune","year":2026,"month":4,"time_utc":"2026-04-15T21:23:14Z"},
{"planet":"neptune","year":2026,"month":5,"time_utc":"2026-05-13T09:11:35Z"},
{"planet":"neptune","year":2026,"month":6,"time_utc":"2026-06-09T19:13:52Z"},
{"planet":"neptune","year":2026,"month":7,"time_utc":"2026-07-07T02:37:58Z"},
{"planet":"neptune","year":2026,"month":8,"time_utc":"2026-08-03T07:56:14Z"},
{"planet":"neptune","year":2026,"month":8,"time_utc":"2026-08-30T12:50:11Z"},
{"planet":"neptune","year":2026,"month":9,"time_utc":"2026-09-26T19:04:28Z"},
{"planet":"neptune","year":2026,"month":10,"time_utc":"2026-10-24T03:16:37Z"},
{"planet":"neptune","year":2026,"month":11,"time_utc":"2026-11-20T12:35:40Z"},
{"planet":"neptune","year":2026,"month":12,"time_utc":"2026-12-17T21:26:40Z"}
]
}
@@ -0,0 +1,92 @@
{
"samples": [
{"planet":"mercury","year":1996,"month":1,"time_utc":"1996-01-20T07:48:44Z"},
{"planet":"mercury","year":1996,"month":2,"time_utc":"1996-02-17T05:58:27Z"},
{"planet":"mercury","year":1996,"month":3,"time_utc":"1996-03-18T21:54:14Z"},
{"planet":"mercury","year":1996,"month":4,"time_utc":"1996-04-19T10:29:52Z"},
{"planet":"mercury","year":1996,"month":5,"time_utc":"1996-05-17T04:02:30Z"},
{"planet":"mercury","year":1996,"month":6,"time_utc":"1996-06-14T00:19:49Z"},
{"planet":"mercury","year":1996,"month":7,"time_utc":"1996-07-16T08:03:39Z"},
{"planet":"mercury","year":1996,"month":8,"time_utc":"1996-08-16T18:24:11Z"},
{"planet":"mercury","year":1996,"month":9,"time_utc":"1996-09-13T13:17:46Z"},
{"planet":"mercury","year":1996,"month":10,"time_utc":"1996-10-11T09:44:59Z"},
{"planet":"mercury","year":1996,"month":11,"time_utc":"1996-11-11T12:53:54Z"},
{"planet":"mercury","year":1996,"month":12,"time_utc":"1996-12-12T05:10:41Z"},
{"planet":"venus","year":1996,"month":1,"time_utc":"1996-01-23T08:25:31Z"},
{"planet":"venus","year":1996,"month":2,"time_utc":"1996-02-22T04:36:35Z"},
{"planet":"venus","year":1996,"month":3,"time_utc":"1996-03-22T23:53:11Z"},
{"planet":"venus","year":1996,"month":4,"time_utc":"1996-04-21T14:13:39Z"},
{"planet":"venus","year":1996,"month":5,"time_utc":"1996-05-20T00:40:14Z"},
{"planet":"venus","year":1996,"month":6,"time_utc":"1996-06-15T09:13:17Z"},
{"planet":"venus","year":1996,"month":7,"time_utc":"1996-07-12T08:38:52Z"},
{"planet":"venus","year":1996,"month":8,"time_utc":"1996-08-10T03:59:30Z"},
{"planet":"venus","year":1996,"month":9,"time_utc":"1996-09-08T23:10:01Z"},
{"planet":"venus","year":1996,"month":10,"time_utc":"1996-10-09T04:07:49Z"},
{"planet":"venus","year":1996,"month":11,"time_utc":"1996-11-08T09:31:53Z"},
{"planet":"venus","year":1996,"month":12,"time_utc":"1996-12-08T13:14:51Z"},
{"planet":"mars","year":1996,"month":1,"time_utc":"1996-01-21T07:43:41Z"},
{"planet":"mars","year":1996,"month":2,"time_utc":"1996-02-19T07:57:56Z"},
{"planet":"mars","year":1996,"month":3,"time_utc":"1996-03-19T07:08:23Z"},
{"planet":"mars","year":1996,"month":4,"time_utc":"1996-04-17T05:16:03Z"},
{"planet":"mars","year":1996,"month":5,"time_utc":"1996-05-16T02:56:01Z"},
{"planet":"mars","year":1996,"month":6,"time_utc":"1996-06-14T00:47:52Z"},
{"planet":"mars","year":1996,"month":7,"time_utc":"1996-07-12T23:06:35Z"},
{"planet":"mars","year":1996,"month":8,"time_utc":"1996-08-10T21:29:02Z"},
{"planet":"mars","year":1996,"month":9,"time_utc":"1996-09-08T19:05:05Z"},
{"planet":"mars","year":1996,"month":10,"time_utc":"1996-10-07T14:58:13Z"},
{"planet":"mars","year":1996,"month":11,"time_utc":"1996-11-05T08:08:58Z"},
{"planet":"mars","year":1996,"month":12,"time_utc":"1996-12-03T21:11:34Z"},
{"planet":"jupiter","year":1996,"month":1,"time_utc":"1996-01-18T19:46:08Z"},
{"planet":"jupiter","year":1996,"month":2,"time_utc":"1996-02-15T15:00:36Z"},
{"planet":"jupiter","year":1996,"month":3,"time_utc":"1996-03-14T06:11:13Z"},
{"planet":"jupiter","year":1996,"month":4,"time_utc":"1996-04-10T16:55:53Z"},
{"planet":"jupiter","year":1996,"month":5,"time_utc":"1996-05-08T00:11:28Z"},
{"planet":"jupiter","year":1996,"month":6,"time_utc":"1996-06-04T05:31:30Z"},
{"planet":"jupiter","year":1996,"month":7,"time_utc":"1996-07-01T10:21:43Z"},
{"planet":"jupiter","year":1996,"month":7,"time_utc":"1996-07-28T15:41:14Z"},
{"planet":"jupiter","year":1996,"month":8,"time_utc":"1996-08-24T22:04:24Z"},
{"planet":"jupiter","year":1996,"month":9,"time_utc":"1996-09-21T05:58:04Z"},
{"planet":"jupiter","year":1996,"month":10,"time_utc":"1996-10-18T16:05:44Z"},
{"planet":"jupiter","year":1996,"month":11,"time_utc":"1996-11-15T05:27:06Z"},
{"planet":"jupiter","year":1996,"month":12,"time_utc":"1996-12-12T22:38:11Z"},
{"planet":"saturn","year":1996,"month":1,"time_utc":"1996-01-24T03:47:15Z"},
{"planet":"saturn","year":1996,"month":2,"time_utc":"1996-02-20T19:10:42Z"},
{"planet":"saturn","year":1996,"month":3,"time_utc":"1996-03-19T10:59:03Z"},
{"planet":"saturn","year":1996,"month":4,"time_utc":"1996-04-16T01:04:52Z"},
{"planet":"saturn","year":1996,"month":5,"time_utc":"1996-05-13T12:31:53Z"},
{"planet":"saturn","year":1996,"month":6,"time_utc":"1996-06-09T21:39:17Z"},
{"planet":"saturn","year":1996,"month":7,"time_utc":"1996-07-07T05:32:21Z"},
{"planet":"saturn","year":1996,"month":8,"time_utc":"1996-08-03T13:13:36Z"},
{"planet":"saturn","year":1996,"month":8,"time_utc":"1996-08-30T21:01:02Z"},
{"planet":"saturn","year":1996,"month":9,"time_utc":"1996-09-27T04:20:31Z"},
{"planet":"saturn","year":1996,"month":10,"time_utc":"1996-10-24T10:21:42Z"},
{"planet":"saturn","year":1996,"month":11,"time_utc":"1996-11-20T15:05:45Z"},
{"planet":"saturn","year":1996,"month":12,"time_utc":"1996-12-17T20:17:06Z"},
{"planet":"uranus","year":1996,"month":1,"time_utc":"1996-01-20T15:54:24Z"},
{"planet":"uranus","year":1996,"month":2,"time_utc":"1996-02-17T05:20:18Z"},
{"planet":"uranus","year":1996,"month":3,"time_utc":"1996-03-15T16:05:29Z"},
{"planet":"uranus","year":1996,"month":4,"time_utc":"1996-04-11T23:42:31Z"},
{"planet":"uranus","year":1996,"month":5,"time_utc":"1996-05-09T05:36:22Z"},
{"planet":"uranus","year":1996,"month":6,"time_utc":"1996-06-05T11:50:19Z"},
{"planet":"uranus","year":1996,"month":7,"time_utc":"1996-07-02T19:33:47Z"},
{"planet":"uranus","year":1996,"month":7,"time_utc":"1996-07-30T04:29:08Z"},
{"planet":"uranus","year":1996,"month":8,"time_utc":"1996-08-26T13:21:06Z"},
{"planet":"uranus","year":1996,"month":9,"time_utc":"1996-09-22T20:54:56Z"},
{"planet":"uranus","year":1996,"month":10,"time_utc":"1996-10-20T03:02:32Z"},
{"planet":"uranus","year":1996,"month":11,"time_utc":"1996-11-16T09:17:15Z"},
{"planet":"uranus","year":1996,"month":12,"time_utc":"1996-12-13T17:54:51Z"},
{"planet":"neptune","year":1996,"month":1,"time_utc":"1996-01-20T07:21:06Z"},
{"planet":"neptune","year":1996,"month":2,"time_utc":"1996-02-16T19:38:46Z"},
{"planet":"neptune","year":1996,"month":3,"time_utc":"1996-03-15T05:08:50Z"},
{"planet":"neptune","year":1996,"month":4,"time_utc":"1996-04-11T11:48:33Z"},
{"planet":"neptune","year":1996,"month":5,"time_utc":"1996-05-08T17:24:20Z"},
{"planet":"neptune","year":1996,"month":6,"time_utc":"1996-06-04T23:59:10Z"},
{"planet":"neptune","year":1996,"month":7,"time_utc":"1996-07-02T08:22:46Z"},
{"planet":"neptune","year":1996,"month":7,"time_utc":"1996-07-29T17:55:53Z"},
{"planet":"neptune","year":1996,"month":8,"time_utc":"1996-08-26T03:10:20Z"},
{"planet":"neptune","year":1996,"month":9,"time_utc":"1996-09-22T10:48:58Z"},
{"planet":"neptune","year":1996,"month":10,"time_utc":"1996-10-19T16:49:59Z"},
{"planet":"neptune","year":1996,"month":11,"time_utc":"1996-11-15T22:54:41Z"},
{"planet":"neptune","year":1996,"month":12,"time_utc":"1996-12-13T07:18:13Z"}
]
}
File diff suppressed because it is too large Load Diff
+1152 -1152
View File
File diff suppressed because it is too large Load Diff
+58
View File
@@ -0,0 +1,58 @@
[
{
"body": "mercury",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 268.524035973,
"declination": -24.001291126,
"ecliptic_longitude": 268.6516112,
"ecliptic_latitude": -0.5700521
},
{
"body": "venus",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 280.056455581,
"declination": -23.622404581,
"ecliptic_longitude": 279.2064734,
"ecliptic_latitude": -0.5050645
},
{
"body": "mars",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 283.879610807,
"declination": -23.720007274,
"ecliptic_longitude": 282.6881475,
"ecliptic_latitude": -0.8911035
},
{
"body": "jupiter",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 113.124332352,
"declination": 21.979135798,
"ecliptic_longitude": 111.3575894,
"ecliptic_latitude": 0.2391257
},
{
"body": "saturn",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 357.380959207,
"declination": -3.596394732,
"ecliptic_longitude": 356.1672313,
"ecliptic_latitude": -2.2587419
},
{
"body": "uranus",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 55.737099009,
"declination": 19.509648526,
"ecliptic_longitude": 57.9492508,
"ecliptic_latitude": -0.1975992
},
{
"body": "neptune",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 0.077612938,
"declination": -1.418610222,
"ecliptic_longitude": 359.5068407,
"ecliptic_latitude": -1.3324096
}
]
+1152 -1152
View File
File diff suppressed because it is too large Load Diff
+1152 -1152
View File
File diff suppressed because it is too large Load Diff
+1728 -1728
View File
File diff suppressed because it is too large Load Diff
+7 -38
View File
@@ -87,53 +87,22 @@ func UranusApparentRaDec(jd float64) (float64, float64) {
}
func EarthUranusAway(jd float64) float64 {
x, y, z := AUranusXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(6, jd, -1)
}
func UranusApparentLo(jd float64) float64 {
x, y, z := AUranusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AUranusXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(6, jd, -1)
return geo.lo
}
func UranusApparentBo(jd float64) float64 {
x, y, z := AUranusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AUranusXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(6, jd, -1)
return geo.bo
}
func UranusApparentLoBo(jd float64) (float64, float64) {
x, y, z := AUranusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AUranusXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo, bo
geo, _ := planetApparentGeocentricPositionN(6, jd, -1)
return geo.lo, geo.bo
}
func UranusMag(jd float64) float64 {
+66 -65
View File
@@ -40,6 +40,28 @@ func uranusSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64 {
return sub
}
func uranusRADerivative(jde, delta float64) float64 {
sub := UranusApparentRa(jde+delta) - UranusApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func uranusRADerivativeN(jde, delta float64, n int) float64 {
sub := UranusApparentRaN(jde+delta, n) - UranusApparentRaN(jde-delta, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func uranusConjunctionFull(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := URANUS_S_PERIOD / 360
@@ -94,113 +116,92 @@ func uranusConjunction(jde, degree float64, next uint8) float64 {
}
func LastUranusConjunction(jde float64) float64 {
return uranusConjunction(jde, 0, 0)
return inclusiveLastPhaseEvent(jde, 0, uranusConjunction)
}
func NextUranusConjunction(jde float64) float64 {
return uranusConjunction(jde, 0, 1)
return inclusiveNextPhaseEvent(jde, 0, uranusConjunction)
}
func LastUranusOpposition(jde float64) float64 {
return uranusConjunction(jde, 180, 0)
return inclusiveLastPhaseEvent(jde, 180, uranusConjunction)
}
func NextUranusOpposition(jde float64) float64 {
return uranusConjunction(jde, 180, 1)
return inclusiveNextPhaseEvent(jde, 180, uranusConjunction)
}
func NextUranusEasternQuadrature(jde float64) float64 {
return uranusConjunction(jde, 90, 1)
return inclusiveNextPhaseEvent(jde, 90, uranusConjunction)
}
func LastUranusEasternQuadrature(jde float64) float64 {
return uranusConjunction(jde, 90, 0)
return inclusiveLastPhaseEvent(jde, 90, uranusConjunction)
}
func NextUranusWesternQuadrature(jde float64) float64 {
return uranusConjunction(jde, 270, 1)
return inclusiveNextPhaseEvent(jde, 270, uranusConjunction)
}
func LastUranusWesternQuadrature(jde float64) float64 {
return uranusConjunction(jde, 270, 0)
return inclusiveLastPhaseEvent(jde, 270, uranusConjunction)
}
func uranusRetrograde(jde float64, searchBeforeOpposition bool) float64 {
//0=last 1=next
raRate := func(jde float64, delta float64) float64 {
sub := UranusApparentRa(jde+delta) - UranusApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
jde = uranusConjunctionFull(jde, 180, 1)
func uranusRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
oppositionTT := TD2UT(oppositionJD, true)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
jde -= 60
easternQuadratureUT := uranusConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
} else {
jde += 60
westernQuadratureUT := uranusConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true)
}
for {
currentRate := raRate(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
rateValue := raRate(prevJD, 2.0/86400.0)
rateSlope := (raRate(prevJD+15.0/86400.0, 2.0/86400.0) - raRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return raRate(jd, 0.5/86400.0)
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return uranusRADerivativeN(jd, 1.0/86400.0, uranusEventSearchN)
}, func(jd float64) float64 {
return uranusRADerivative(jd, 0.5/86400.0)
})
return TD2UT(bestJD, false)
}
func NextUranusRetrogradeToPrograde(jde float64) float64 {
date := uranusRetrograde(jde, false)
if date < jde {
oppositionJD := uranusConjunctionFull(jde, 180, 1)
return uranusRetrograde(oppositionJD+10, false)
}
lastOppositionJD := uranusConjunctionFull(jde, 180, 0)
date := uranusRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextOppositionJD := uranusConjunctionFull(jde, 180, 1)
return uranusRetrogradeAroundOpposition(nextOppositionJD, false)
}
func LastUranusRetrogradeToPrograde(jde float64) float64 {
jde = uranusConjunctionFull(jde, 180, 0) - 10
date := uranusRetrograde(jde, false)
if date > jde {
oppositionJD := uranusConjunctionFull(jde, 180, 0)
return uranusRetrograde(oppositionJD-10, false)
}
lastOppositionJD := uranusConjunctionFull(jde, 180, 0)
date := uranusRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
previousOppositionJD := uranusConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return uranusRetrogradeAroundOpposition(previousOppositionJD, false)
}
func NextUranusProgradeToRetrograde(jde float64) float64 {
date := uranusRetrograde(jde, true)
if date < jde {
oppositionJD := uranusConjunctionFull(jde, 180, 1)
return uranusRetrograde(oppositionJD+10, true)
}
nextOppositionJD := uranusConjunctionFull(jde, 180, 1)
date := uranusRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
followingOppositionJD := uranusConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return uranusRetrogradeAroundOpposition(followingOppositionJD, true)
}
func LastUranusProgradeToRetrograde(jde float64) float64 {
jde = uranusConjunctionFull(jde, 180, 0) - 10
date := uranusRetrograde(jde, true)
if date > jde {
oppositionJD := uranusConjunctionFull(jde, 180, 0)
return uranusRetrograde(oppositionJD-10, true)
}
nextOppositionJD := uranusConjunctionFull(jde, 180, 1)
date := uranusRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
lastOppositionJD := uranusConjunctionFull(jde, 180, 0)
return uranusRetrogradeAroundOpposition(lastOppositionJD, true)
}
+7 -38
View File
@@ -87,53 +87,22 @@ func VenusApparentRaDec(jd float64) (float64, float64) {
}
func EarthVenusAway(jd float64) float64 {
x, y, z := AVenusXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(2, jd, -1)
}
func VenusApparentLo(jd float64) float64 {
x, y, z := AVenusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AVenusXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(2, jd, -1)
return geo.lo
}
func VenusApparentBo(jd float64) float64 {
x, y, z := AVenusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AVenusXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(2, jd, -1)
return geo.bo
}
func VenusApparentLoBo(jd float64) (float64, float64) {
x, y, z := AVenusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AVenusXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo, bo
geo, _ := planetApparentGeocentricPositionN(2, jd, -1)
return geo.lo, geo.bo
}
func VenusMag(jd float64) float64 {
+415 -156
View File
@@ -3,6 +3,7 @@ package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
@@ -33,6 +34,23 @@ func venusSunLongitudeDeltaN(jde float64, n int) float64 {
return sub
}
func venusConjunctionAngleDelta(diff float64) float64 {
diff = Limit360(diff)
if diff > 180 {
diff -= 360
}
if diff < -180 {
diff += 360
}
return diff
}
func venusConjunctionHeliocentricDelta(jd, targetDeg float64, n int) float64 {
planetLo := planet.WherePlanetN(2, 0, jd, n)
earthLo := planet.WherePlanetN(-1, 0, jd, n)
return venusConjunctionAngleDelta(planetLo - earthLo - targetDeg)
}
func venusSunRADelta(jde float64) float64 {
sub := Limit360(VenusApparentRa(jde) - SunApparentRa(jde))
if sub > 180 {
@@ -66,13 +84,94 @@ func venusRADerivativeN(jde, val float64, n int) float64 {
return sub / (2 * val)
}
func venusRAContinuousForMax(jde float64) float64 {
ra := VenusApparentRa(jde)
if ra < 180 {
return ra + 360
}
return ra
}
func venusRAContinuousForMaxN(jde float64, n int) float64 {
ra := VenusApparentRaN(jde, n)
if ra < 180 {
return ra + 360
}
return ra
}
func venusRAContinuousForMin(jde float64) float64 {
ra := VenusApparentRa(jde)
if ra > 180 {
return ra - 360
}
return ra
}
func venusRAContinuousForMinN(jde float64, n int) float64 {
ra := VenusApparentRaN(jde, n)
if ra > 180 {
return ra - 360
}
return ra
}
func venusRAExtremumRefine(seed, start, end, step float64, fn func(float64) float64) float64 {
centerJD := clampFloat64(seed, start, end)
halfStep := step
bestJD := centerJD
bestVal := fn(centerJD)
for i := 0; i < 8; i++ {
leftJD := clampFloat64(centerJD-halfStep, start, end)
rightJD := clampFloat64(centerJD+halfStep, start, end)
leftVal := fn(leftJD)
centerVal := fn(centerJD)
rightVal := fn(rightJD)
if leftVal > bestVal {
bestVal = leftVal
bestJD = leftJD
}
if centerVal > bestVal {
bestVal = centerVal
bestJD = centerJD
}
if rightVal > bestVal {
bestVal = rightVal
bestJD = rightJD
}
denominator := leftVal - 2*centerVal + rightVal
if denominator == 0 {
centerJD = bestJD
halfStep /= 2
continue
}
vertexJD := centerJD + 0.5*halfStep*(leftVal-rightVal)/denominator
vertexJD = clampFloat64(vertexJD, leftJD, rightJD)
vertexVal := fn(vertexJD)
if vertexVal > bestVal {
bestVal = vertexVal
bestJD = vertexJD
}
centerJD = bestJD
halfStep /= 2
}
return bestJD
}
func venusSunElongationN(jde float64, n int) float64 {
lo1, bo1 := VenusApparentLoBoN(jde, n)
lo2 := SunApparentLo(jde)
lo2 := HSunApparentLoN(jde, n)
bo2 := HSunTrueBoN(jde, n)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
func venusTrueElongationN(jde float64, n int) float64 {
earth := mercuryHelioN(-1, jde, n)
planetPos := mercuryHelioN(2, jde, n)
geo := mercuryGeocentric(planetPos, earth)
return StarAngularSeparation(geo.lo, geo.bo, HSunTrueLoN(jde, n), HSunTrueBoN(jde, n))
}
func venusElongationDerivative(jde, val float64) float64 {
sub := VenusSunElongation(jde+val) - VenusSunElongation(jde-val)
if sub > 180 {
@@ -96,91 +195,102 @@ func venusElongationDerivativeN(jde, val float64, n int) float64 {
}
func venusConjunction(jde float64, next uint8) float64 {
//0=last 1=next
nowSub := venusSunLongitudeDeltaN(jde, venusEventSearchN)
pos := math.Abs(venusSunLongitudeDeltaN(jde+1/86400.0, venusEventSearchN)) - math.Abs(nowSub)
if pos >= 0 && next == 1 && nowSub > 0 {
jde += VENUS_S_PERIOD/8.0 + 2
}
if pos >= 0 && next == 1 && nowSub < 0 {
jde += VENUS_S_PERIOD/6.0 + 2
}
if pos <= 0 && next == 0 && nowSub < 0 {
jde -= VENUS_S_PERIOD/8.0 + 2
}
if pos <= 0 && next == 0 && nowSub > 0 {
jde -= VENUS_S_PERIOD/6.0 + 2
}
for {
nowSub := venusSunLongitudeDeltaN(jde, venusEventSearchN)
pos := math.Abs(venusSunLongitudeDeltaN(jde+1/86400.0, venusEventSearchN)) - math.Abs(nowSub)
if math.Abs(nowSub) > 24 || (pos > 0 && next == 1) || (pos < 0 && next == 0) {
queryTT := jde
direction := -1.0
if next == 1 {
jde += 8
} else {
jde -= 8
direction = 1
}
continue
left := queryTT
leftVal := venusSunLongitudeDeltaN(left, venusEventSearchN)
if math.Abs(venusSunLongitudeDelta(queryTT)) <= 30.0/86400.0 {
exact := eventZeroRefine(left, 1.0, 0.000005, venusSunLongitudeDelta)
eventUT := TD2UT(exact, false)
if next == 0 && eventUTQueryBeforeOrEqual(eventUT, queryTT) {
return eventUT
}
break
}
JD1 := jde
for {
JD0 := JD1
stDegree := venusSunLongitudeDelta(JD0)
stDegreep := (venusSunLongitudeDelta(JD0+0.000005) - venusSunLongitudeDelta(JD0-0.000005)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
break
if next == 1 && eventUTQueryAfterOrEqual(eventUT, queryTT) {
return eventUT
}
}
return TD2UT(JD1, false)
const step = 8.0
for i := 0; i < 80; i++ {
right := queryTT + direction*step*float64(i+1)
rightVal := venusSunLongitudeDeltaN(right, venusEventSearchN)
if leftVal == 0 || rightVal == 0 || leftVal*rightVal <= 0 {
center := (left + right) / 2.0
halfWindow := math.Abs(right-left) / 2.0
return TD2UT(eventZeroRefine(center, halfWindow, 0.000005, venusSunLongitudeDelta), false)
}
left = right
leftVal = rightVal
}
return TD2UT(eventZeroRefine(queryTT, VENUS_S_PERIOD, 0.000005, venusSunLongitudeDelta), false)
}
func venusConjunctionTypeAt(eventUT float64) bool {
return EarthVenusAway(eventUT) <= EarthAway(eventUT)
}
func nextVenusTypedConjunctionFromEvent(jde float64, inferior bool) float64 {
date := NextVenusConjunctionStrict(jde)
if venusConjunctionTypeAt(date) == inferior {
return date
}
return NextVenusConjunctionStrict(eventUTNextQueryTT(date))
}
func lastVenusTypedConjunctionFromEvent(jde float64, inferior bool) float64 {
date := LastVenusConjunctionStrict(jde)
if venusConjunctionTypeAt(date) == inferior {
return date
}
return LastVenusConjunctionStrict(eventUTLastQueryTT(date))
}
func LastVenusConjunction(jde float64) float64 {
return venusConjunction(jde, 0)
return inclusiveLastSimpleEvent(jde, LastVenusConjunctionStrict, NextVenusConjunctionStrict)
}
func NextVenusConjunction(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusConjunctionStrict, NextVenusConjunctionStrict)
}
func LastVenusConjunctionStrict(jde float64) float64 {
return venusConjunction(jde, 0)
}
func NextVenusConjunctionStrict(jde float64) float64 {
return venusConjunction(jde, 1)
}
func NextVenusInferiorConjunction(jde float64) float64 {
date := NextVenusConjunction(jde)
if EarthVenusAway(date) > EarthAway(date) {
return NextVenusConjunction(date + 2)
func nextVenusTypedConjunction(jde float64, inferior bool) float64 {
return nextVenusTypedConjunctionFromEvent(jde, inferior)
}
return date
func lastVenusTypedConjunction(jde float64, inferior bool) float64 {
return lastVenusTypedConjunctionFromEvent(jde, inferior)
}
func NextVenusInferiorConjunction(jde float64) float64 {
return nextVenusTypedConjunction(jde, true)
}
func NextVenusSuperiorConjunction(jde float64) float64 {
date := NextVenusConjunction(jde)
if EarthVenusAway(date) < EarthAway(date) {
return NextVenusConjunction(date + 2)
}
return date
return nextVenusTypedConjunction(jde, false)
}
func LastVenusInferiorConjunction(jde float64) float64 {
date := LastVenusConjunction(jde)
if EarthVenusAway(date) > EarthAway(date) {
return LastVenusConjunction(date - 2)
}
return date
return lastVenusTypedConjunction(jde, true)
}
func LastVenusSuperiorConjunction(jde float64) float64 {
date := LastVenusConjunction(jde)
if EarthVenusAway(date) < EarthAway(date) {
return LastVenusConjunction(date - 2)
}
return date
return lastVenusTypedConjunction(jde, false)
}
func venusRetrograde(jde float64) float64 {
//0=last 1=next
lastHe := LastVenusConjunction(jde)
nextHe := NextVenusConjunction(jde)
lastHe := LastVenusConjunctionStrict(jde)
nextHe := NextVenusConjunctionStrict(jde)
nowSub := venusSunRADelta(jde)
if nowSub > 0 {
jde = lastHe + ((nextHe - lastHe) / 5.0 * 3.5)
@@ -213,152 +323,301 @@ func venusRetrograde(jde float64) float64 {
}
func NextVenusRetrograde(jde float64) float64 {
date := venusRetrograde(jde)
if date < jde {
nextHe := NextVenusConjunction(jde)
return venusRetrograde(nextHe + 2)
p2r := NextVenusProgradeToRetrograde(jde)
r2p := NextVenusRetrogradeToPrograde(jde)
if sameEventJD(p2r, r2p) {
return p2r
}
return date
if p2r < r2p {
return p2r
}
return r2p
}
func LastVenusRetrograde(jde float64) float64 {
lastHe := LastVenusConjunction(jde)
date := venusRetrograde(lastHe + 2)
if date > jde {
lastLastHe := LastVenusConjunction(lastHe - 2)
return venusRetrograde(lastLastHe + 2)
p2r := LastVenusProgradeToRetrograde(jde)
r2p := LastVenusRetrogradeToPrograde(jde)
if sameEventJD(p2r, r2p) {
return p2r
}
return date
if p2r > r2p {
return p2r
}
return r2p
}
func venusStationInWindow(start, end float64, progradeToRetrograde bool) float64 {
var best float64
if progradeToRetrograde {
guess := scanWindowForMax(start, end, 2.0, func(jd float64) float64 {
return venusRAContinuousForMaxN(jd, venusEventSearchN)
})
best = venusRAExtremumRefine(guess, start, end, 1.0, func(jd float64) float64 {
return venusRAContinuousForMax(jd)
})
} else {
guess := scanWindowForMax(start, end, 2.0, func(jd float64) float64 {
return -venusRAContinuousForMinN(jd, venusEventSearchN)
})
best = venusRAExtremumRefine(guess, start, end, 1.0, func(jd float64) float64 {
return -venusRAContinuousForMin(jd)
})
}
return TD2UT(best, false)
}
func venusProgradeToRetrogradeAroundInferior(inferior float64) float64 {
return venusStationInWindow(inferior-30.0, inferior-14.0, true)
}
func venusRetrogradeToProgradeAroundInferior(inferior float64) float64 {
return venusStationInWindow(inferior+14.0, inferior+24.0, false)
}
func NextVenusProgradeToRetrograde(jde float64) float64 {
date := NextVenusRetrograde(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return NextVenusRetrograde(date + VENUS_S_PERIOD/2)
}
inferior := NextVenusInferiorConjunction(jde)
for {
date := venusProgradeToRetrogradeAroundInferior(inferior)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
inferior = NextVenusInferiorConjunction(eventUTNextQueryTT(inferior))
}
}
func NextVenusRetrogradeToPrograde(jde float64) float64 {
date := NextVenusRetrograde(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return NextVenusRetrograde(date + 12)
}
inferior := LastVenusInferiorConjunction(jde)
for {
date := venusRetrogradeToProgradeAroundInferior(inferior)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
inferior = NextVenusInferiorConjunction(eventUTNextQueryTT(inferior))
}
}
func LastVenusProgradeToRetrograde(jde float64) float64 {
date := LastVenusRetrograde(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return LastVenusRetrograde(date - 12)
}
inferior := NextVenusInferiorConjunction(jde)
for {
date := venusProgradeToRetrogradeAroundInferior(inferior)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
inferior = LastVenusInferiorConjunction(eventUTLastQueryTT(inferior))
}
}
func LastVenusRetrogradeToPrograde(jde float64) float64 {
date := LastVenusRetrograde(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return LastVenusRetrograde(date - VENUS_S_PERIOD/2)
}
inferior := LastVenusInferiorConjunction(jde)
for {
date := venusRetrogradeToProgradeAroundInferior(inferior)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
inferior = LastVenusInferiorConjunction(eventUTLastQueryTT(inferior))
}
}
func VenusSunElongation(jde float64) float64 {
lo1, bo1 := VenusApparentLoBo(jde)
lo2 := SunApparentLo(jde)
lo2 := HSunApparentLo(jde)
bo2 := HSunTrueBo(jde)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
func venusGreatestElongation(jde float64) float64 {
lastHe := LastVenusConjunction(jde)
nextHe := NextVenusConjunction(jde)
nowSub := venusSunRADelta(jde)
if nowSub > 0 {
jde = lastHe + ((nextHe - lastHe) / 5.0 * 2.5)
} else {
jde = lastHe + ((nextHe - lastHe) / 5.0)
}
for {
nowSub := venusElongationDerivativeN(jde, 1.0/86400.0, venusEventSearchN)
if math.Abs(nowSub) > 0.15 {
jde += 5
continue
}
break
}
JD1 := jde
for {
JD0 := JD1
stDegree := venusElongationDerivative(JD0, 2.0/86400.0)
stDegreep := (venusElongationDerivative(JD0+15.0/86400.0, 2.0/86400.0) - venusElongationDerivative(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
}
}
min := eventZeroRefine(JD1, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return venusElongationDerivative(jd, 0.5/86400.0)
func venusGreatestElongationInWindow(start, end float64) float64 {
best := maximizeInWindow(start, end, 5.0, func(jd float64) float64 {
return venusTrueElongationN(jd, venusEventSearchN)
}, func(jd float64) float64 {
return venusTrueElongationN(jd, -1)
})
//fmt.Println((min - lastHe) / (nextHe - lastHe))
return TD2UT(min, false)
return TD2UT(best, false)
}
func venusEastElongationWindowEndingAt(inferior float64) (float64, float64) {
lastSuperior := LastVenusSuperiorConjunction(eventUTLastQueryTT(inferior))
return lastSuperior + innerEventWindowPadding, inferior - innerEventWindowPadding
}
func venusWestElongationWindowEndingAt(superior float64) (float64, float64) {
lastInferior := LastVenusInferiorConjunction(eventUTLastQueryTT(superior))
return lastInferior + innerEventWindowPadding, superior - innerEventWindowPadding
}
func venusEastElongationWindowContaining(jde float64) (float64, float64) {
nextInferior := NextVenusInferiorConjunction(jde)
start, end := venusEastElongationWindowEndingAt(nextInferior)
if eventUTQueryBeforeOrEqual(start, jde) && eventUTQueryAfterOrEqual(end, jde) {
return start, end
}
currentInferior := LastVenusInferiorConjunction(jde)
return venusEastElongationWindowEndingAt(currentInferior)
}
func venusWestElongationWindowContaining(jde float64) (float64, float64) {
nextSuperior := NextVenusSuperiorConjunction(jde)
start, end := venusWestElongationWindowEndingAt(nextSuperior)
if eventUTQueryBeforeOrEqual(start, jde) && eventUTQueryAfterOrEqual(end, jde) {
return start, end
}
currentSuperior := LastVenusSuperiorConjunction(jde)
return venusWestElongationWindowEndingAt(currentSuperior)
}
func nextVenusGreatestElongationTyped(jde float64, east bool) float64 {
if east {
start, windowEnd := venusEastElongationWindowContaining(jde)
for {
date := venusGreatestElongationInWindow(start, windowEnd)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextInferior := NextVenusInferiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = venusEastElongationWindowEndingAt(nextInferior)
}
}
start, windowEnd := venusWestElongationWindowContaining(jde)
for {
date := venusGreatestElongationInWindow(start, windowEnd)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextSuperior := NextVenusSuperiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = venusWestElongationWindowEndingAt(nextSuperior)
}
}
func lastVenusGreatestElongationTyped(jde float64, east bool) float64 {
if east {
start, windowEnd := venusEastElongationWindowContaining(jde)
for {
date := venusGreatestElongationInWindow(start, windowEnd)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevInferior := LastVenusInferiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = venusEastElongationWindowEndingAt(prevInferior)
}
}
start, windowEnd := venusWestElongationWindowContaining(jde)
for {
date := venusGreatestElongationInWindow(start, windowEnd)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevSuperior := LastVenusSuperiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = venusWestElongationWindowEndingAt(prevSuperior)
}
}
func venusGreatestElongation(jde float64) float64 {
east := venusSunRADelta(jde) > 0
if east {
return nextVenusGreatestElongationTyped(jde, true)
}
return nextVenusGreatestElongationTyped(jde, false)
}
func NextVenusGreatestElongation(jde float64) float64 {
date := venusGreatestElongation(jde)
if date < jde {
nextHe := NextVenusConjunction(jde)
return venusGreatestElongation(nextHe + 2)
east := NextVenusGreatestElongationEast(jde)
west := NextVenusGreatestElongationWest(jde)
if sameEventJD(east, west) {
return east
}
return date
if east < west {
return east
}
return west
}
func LastVenusGreatestElongation(jde float64) float64 {
lastHe := LastVenusConjunction(jde)
date := venusGreatestElongation(lastHe + 2)
if date > jde {
lastLastHe := LastVenusConjunction(lastHe - 2)
return venusGreatestElongation(lastLastHe + 2)
east := LastVenusGreatestElongationEast(jde)
west := LastVenusGreatestElongationWest(jde)
if sameEventJD(east, west) {
return east
}
return date
if east > west {
return east
}
return west
}
func LastVenusInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusInferiorConjunction, NextVenusInferiorConjunction)
}
func NextVenusInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusInferiorConjunction, NextVenusInferiorConjunction)
}
func LastVenusSuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusSuperiorConjunction, NextVenusSuperiorConjunction)
}
func NextVenusSuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusSuperiorConjunction, NextVenusSuperiorConjunction)
}
func LastVenusRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusRetrograde, NextVenusRetrograde)
}
func NextVenusRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusRetrograde, NextVenusRetrograde)
}
func LastVenusProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusProgradeToRetrograde, NextVenusProgradeToRetrograde)
}
func NextVenusProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusProgradeToRetrograde, NextVenusProgradeToRetrograde)
}
func LastVenusRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusRetrogradeToPrograde, NextVenusRetrogradeToPrograde)
}
func NextVenusRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusRetrogradeToPrograde, NextVenusRetrogradeToPrograde)
}
func LastVenusGreatestElongationInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusGreatestElongation, NextVenusGreatestElongation)
}
func NextVenusGreatestElongationInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusGreatestElongation, NextVenusGreatestElongation)
}
func LastVenusGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusGreatestElongationEast, NextVenusGreatestElongationEast)
}
func NextVenusGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusGreatestElongationEast, NextVenusGreatestElongationEast)
}
func LastVenusGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusGreatestElongationWest, NextVenusGreatestElongationWest)
}
func NextVenusGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusGreatestElongationWest, NextVenusGreatestElongationWest)
}
func NextVenusGreatestElongationEast(jde float64) float64 {
date := NextVenusGreatestElongation(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return NextVenusGreatestElongation(date + 1)
}
return date
return nextVenusGreatestElongationTyped(jde, true)
}
func NextVenusGreatestElongationWest(jde float64) float64 {
date := NextVenusGreatestElongation(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return NextVenusGreatestElongation(date + 1)
}
return date
return nextVenusGreatestElongationTyped(jde, false)
}
func LastVenusGreatestElongationEast(jde float64) float64 {
date := LastVenusGreatestElongation(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return LastVenusGreatestElongation(date - 1)
}
return date
return lastVenusGreatestElongationTyped(jde, true)
}
func LastVenusGreatestElongationWest(jde float64) float64 {
date := LastVenusGreatestElongation(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return LastVenusGreatestElongation(date - 1)
}
return date
return lastVenusGreatestElongationTyped(jde, false)
}
+41
View File
@@ -50,6 +50,10 @@ const (
// 返回 农历月,日,是否闰月以及文字描述
// 按现行农历GB/T 33661-2017算法计算,推荐使用年限为[1929-3000]年
// 古代由于定朔定气误差此处计算会与古时不符
// Inputs are civil year, month, day, and timezone offset in hours.
// Returns the lunar year, month, day, leap-month flag, and text description.
// The current GB/T 33661-2017 lunar-calendar convention is recommended for years 1929-3000.
// For ancient dates, the result may differ from historical practice because computed new-moon and solar-term reconstructions are approximate for ancient dates.
func Lunar(year, month, day int, timezone float64) (int, int, int, bool, string) {
return basic.GetLunar(year, month, day, timezone/24.0)
}
@@ -62,6 +66,12 @@ func Lunar(year, month, day int, timezone float64) (int, int, int, bool, string)
// 由于农历还未到鼠年,故应当传入Solar(2019,12,30,false)
// 按现行农历GB/T 33661-2017算法计算,推荐使用年限为[1929-3000]年
// 古代由于定朔定气误差此处计算会与古时不符
// Inputs are the civil-year proxy of the lunar year, lunar month, lunar day, leap-month flag, and timezone offset in hours.
// Returns the corresponding civil time.
// The lunar year parameter follows the civil year containing the lunar New Year of that cycle.
// For example, the last day of the Ji-Hai year corresponds to 2020-01-24, but should still be passed as `Solar(2019, 12, 30, false, ...)`.
// The current GB/T 33661-2017 lunar-calendar convention is recommended for years 1929-3000.
// For ancient dates, the result may differ from historical practice because computed new-moon and solar-term reconstructions are approximate for ancient dates.
func Solar(year, month, day int, leap bool, timezone float64) time.Time {
jde := basic.GetSolar(year, month, day, leap, timezone/24.0)
zone := time.FixedZone("CST", int(timezone*3600))
@@ -74,6 +84,11 @@ func Solar(year, month, day int, leap bool, timezone float64) time.Time {
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Input is a civil `time.Time`.
// Returns a `Time` value carrying the lunar-calendar information.
// Supported years are [-103, 3000].
// Years [-103, 1912] use the historical-calendar tables included in this package.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func SolarToLunar(date time.Time) (Time, error) {
return innerSolarToLunar(date)
}
@@ -84,6 +99,11 @@ func SolarToLunar(date time.Time) (Time, error) {
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Inputs are the civil year, month, and day.
// Returns a `Time` value carrying the lunar-calendar information.
// Supported years are [-103, 3000].
// Years [-103, 1912] use the historical-calendar tables included in this package.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func SolarToLunarByYMD(year, month, day int) (Time, error) {
return innerSolarToLunarByYMD(year, month, day)
}
@@ -165,6 +185,14 @@ func transformModenLunar2Time(date time.Time, year, month, day int, leap bool, d
// 年号+农历月中文描述+农历日中文描述
// 年号+农历月中文描述+干支日中文描述
// 支持年份:[-103,3000]
// Input is a lunar-date description such as `二零二零年正月初一`, `元丰六年十月十二`, or `元嘉二十七年七月庚午日`.
// Returns all matching `Time` results with both civil and lunar information.
// The parser accepts these forms:
// lunar year text + lunar month text + lunar day text
// lunar year text + lunar month text + sexagenary day text
// era name + lunar month text + lunar day text
// era name + lunar month text + sexagenary day text
// Supported years are [-103, 3000].
func LunarToSolar(desc string) ([]Time, error) {
dates, err := innerParseLunar(desc)
if err != nil {
@@ -189,6 +217,12 @@ func LunarToSolar(desc string) ([]Time, error) {
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息,注意,这里农历月份代表的是以当时的历法推定的农历月与正月的距离,正月为1,二月为2,依次类推,闰月显示所闰月
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Deprecated: use LunarToSolarByYMD.
// Inputs are lunar year, month, day, and the leap-month flag.
// Returns a `Time` value carrying both civil and lunar information.
// Supported years are [-103, 3000].
// For years [-103, 1912], the lunar month index follows the historical calendar in force at that time, counted from the first month of that year.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func LunarToSolarSingle(year, month, day int, leap bool) (Time, error) {
return LunarToSolarByYMD(year, month, day, leap)
}
@@ -199,6 +233,11 @@ func LunarToSolarSingle(year, month, day int, leap bool) (Time, error) {
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息,注意,这里农历月份代表的是以当时的历法推定的农历月与正月的距离,正月为1,二月为2,依次类推,闰月显示所闰月
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Inputs are lunar year, month, day, and the leap-month flag.
// Returns a `Time` value carrying both civil and lunar information.
// Supported years are [-103, 3000].
// For years [-103, 1912], the lunar month index follows the historical calendar in force at that time, counted from the first month of that year.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func LunarToSolarByYMD(year, month, day int, leap bool) (Time, error) {
if year < -103 || year > 9999 {
return Time{}, fmt.Errorf("年份超出范围")
@@ -218,6 +257,7 @@ func LunarToSolarByYMD(year, month, day int, leap bool) (Time, error) {
// JieQi 节气时刻(北京时间) / solar term instant in Beijing time.
//
// 返回传入年份、节气对应的北京时间节气时间。
// Returns the Beijing-time instant of the requested solar term in the supplied year.
func JieQi(year, term int) time.Time {
calcJde := basic.GetJQTime(year, term)
zone := time.FixedZone("CST", 8*3600)
@@ -227,6 +267,7 @@ func JieQi(year, term int) time.Time {
// WuHou 物候时刻(北京时间) / pentad instant in Beijing time.
//
// 返回传入年份、物候对应的北京时间物候时间。
// Returns the Beijing-time instant of the requested pentad in the supplied year.
func WuHou(year, term int) time.Time {
calcJde := basic.GetWuHouTime(year, term)
zone := time.FixedZone("CST", 8*3600)
+18
View File
@@ -52,6 +52,7 @@ type Time struct {
// Solar 公历时间 / solar time.
//
// 返回内部保存的公历 `time.Time`,不做时区或历法再计算。
// Returns the stored civil `time.Time` directly, without any further time-zone or calendar conversion.
func (t Time) Solar() time.Time {
return t.solarTime
}
@@ -59,6 +60,7 @@ func (t Time) Solar() time.Time {
// Time 公历时间 / solar time.
//
// 是 `Solar` 的同义接口,便于把 `calendar.Time` 当作普通时间对象使用。
// This is an alias of `Solar`, convenient when `calendar.Time` is used as a regular time object.
func (t Time) Time() time.Time {
return t.solarTime
}
@@ -66,6 +68,7 @@ func (t Time) Time() time.Time {
// Lunars 农历候选结果 / lunar candidates.
//
// 返回全部候选农历结果。
// Returns all candidate lunar-calendar results.
func (t Time) Lunars() []LunarTime {
return t.lunars
}
@@ -73,6 +76,7 @@ func (t Time) Lunars() []LunarTime {
// LunarDesc 农历描述 / lunar descriptions.
//
// 返回全部候选结果的农历描述,如开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the lunar-date descriptions for all candidates. If no era name is available, the year is described directly.
func (t Time) LunarDesc() []string {
var res []string
for _, v := range t.lunars {
@@ -84,6 +88,7 @@ func (t Time) LunarDesc() []string {
// LunarDescWithEmperor 含君主信息的农历描述 / lunar descriptions with emperor.
//
// 返回全部候选结果中含有君主信息的农历描述,如唐玄宗 开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the candidate descriptions with emperor names when available. If no era name is available, the year is described directly.
func (t Time) LunarDescWithEmperor() []string {
var res []string
for _, v := range t.lunars {
@@ -95,6 +100,7 @@ func (t Time) LunarDescWithEmperor() []string {
// LunarDescWithDynasty 含朝代信息的农历描述 / lunar descriptions with dynasty.
//
// 返回全部候选结果中含有朝代信息的农历描述,如唐 开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the candidate descriptions with dynasty names when available. If no era name is available, the year is described directly.
func (t Time) LunarDescWithDynasty() []string {
var res []string
for _, v := range t.lunars {
@@ -106,6 +112,7 @@ func (t Time) LunarDescWithDynasty() []string {
// LunarDescWithDynastyAndEmperor 含朝代与君主信息的农历描述 / lunar descriptions with dynasty and emperor.
//
// 返回全部候选结果中含有朝代和君主信息的农历描述,如唐 唐玄宗 开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the candidate descriptions with both dynasty and emperor names when available. If no era name is available, the year is described directly.
func (t Time) LunarDescWithDynastyAndEmperor() []string {
var res []string
for _, v := range t.lunars {
@@ -117,6 +124,7 @@ func (t Time) LunarDescWithDynastyAndEmperor() []string {
// LunarInfo 农历结构化信息 / structured lunar information.
//
// 返回全部候选结果对应的结构化农历信息切片。
// Returns the structured lunar-calendar information for all candidates.
func (t Time) LunarInfo() []LunarInfo {
var res []LunarInfo
for _, v := range t.lunars {
@@ -128,6 +136,7 @@ func (t Time) LunarInfo() []LunarInfo {
// Eras 朝代、皇帝、年号信息 / era information.
//
// 返回全部候选结果对应的朝代、皇帝、年号信息。
// Returns the dynasty, emperor, and era-name records associated with all candidates.
func (t Time) Eras() []EraDesc {
var res []EraDesc
for _, v := range t.lunars {
@@ -139,6 +148,7 @@ func (t Time) Eras() []EraDesc {
// Lunar 首个农历结果 / first lunar result.
//
// 若存在多个候选结果,只返回第一个;无结果时返回零值 `LunarTime`。
// Returns only the first candidate when multiple results exist. A zero-value `LunarTime` is returned when no result is available.
func (t Time) Lunar() LunarTime {
if len(t.lunars) > 0 {
return t.lunars[0]
@@ -149,6 +159,7 @@ func (t Time) Lunar() LunarTime {
// Add 时间偏移 / add a duration.
//
// 返回公历时间偏移后的农历结果。
// Returns the lunar-calendar result after applying the duration to the stored civil time.
func (t Time) Add(d time.Duration) Time {
if d < time.Second {
newT := t.solarTime.Add(d)
@@ -236,6 +247,7 @@ func (l LunarTime) IsLeap() bool {
// Eras 朝代、皇帝、年号信息 / era information.
//
// 返回该农历结果对应的朝代、皇帝、年号信息。
// Returns the dynasty, emperor, and era-name records associated with this lunar result.
func (l LunarTime) Eras() []EraDesc {
return l.eras
}
@@ -243,6 +255,7 @@ func (l LunarTime) Eras() []EraDesc {
// MonthDay 农历月日描述 / lunar month-day description.
//
// 获取农历月日描述,如正月初一。此处,十一月表示为冬月,十二月表示为腊月。
// Returns the lunar month-day description, such as `正月初一`. In this package, month 11 is written as `冬月` and month 12 as `腊月`.
func (l LunarTime) MonthDay() string {
return l.desc
}
@@ -250,6 +263,7 @@ func (l LunarTime) MonthDay() string {
// LunarDesc 农历描述 / lunar descriptions.
//
// 获取农历描述,如开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the lunar-date descriptions for this result. If no era name is available, the year is described directly.
func (l LunarTime) LunarDesc() []string {
return l.innerDescWithNianHao(false, false)
}
@@ -258,6 +272,7 @@ func (l LunarTime) LunarDesc() []string {
//
// 获取含有君主信息的农历描述,如唐玄宗 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
// 君主信息仅供参考,多个皇帝用同一个年号的场景,此处不准
// Returns the lunar-date descriptions with emperor names when available. Emperor names are for reference only and may be ambiguous when multiple emperors used the same era name.
func (l LunarTime) LunarDescWithEmperor() []string {
return l.innerDescWithNianHao(true, false)
}
@@ -265,6 +280,7 @@ func (l LunarTime) LunarDescWithEmperor() []string {
// LunarDescWithDynasty 含朝代信息的农历描述 / lunar descriptions with dynasty.
//
// 获取含有朝代信息的农历描述,如唐 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the lunar-date descriptions with dynasty names when available. If no era name is available, the year is described directly.
func (l LunarTime) LunarDescWithDynasty() []string {
return l.innerDescWithNianHao(false, true)
}
@@ -273,6 +289,7 @@ func (l LunarTime) LunarDescWithDynasty() []string {
//
// 获取含有朝代和君主信息的农历描述,如唐 唐玄宗 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
// 君主信息仅供参考,多个皇帝用同一个年号的场景,此处不准
// Returns the lunar-date descriptions with both dynasty and emperor names when available. Emperor names are for reference only and may be ambiguous when multiple emperors used the same era name.
func (l LunarTime) LunarDescWithDynastyAndEmperor() []string {
return l.innerDescWithNianHao(true, true)
}
@@ -299,6 +316,7 @@ func (l LunarTime) innerDescWithNianHao(withEmperor bool, withDynasty bool) []st
// LunarInfo 农历结构化信息 / structured lunar information.
//
// 返回该农历结果对应的结构化农历信息切片;若存在多个并行年号,则会有多条记录。
// Returns the structured lunar-calendar information for this result. Multiple records are returned when parallel era-name interpretations exist.
func (l LunarTime) LunarInfo() []LunarInfo {
var res []LunarInfo
for _, v := range l.eras {
+5
View File
@@ -5,6 +5,7 @@ import "b612.me/astro/formula"
// AirmassPlaneParallelFromTrueAltitude 平行平板大气质量 / plane-parallel airmass from true altitude.
//
// 输入为真高度角,单位度。适合中高空几何近似,接近地平线时会发散。
// Input is true altitude in degrees. This geometric approximation is suitable at moderate and high altitudes but diverges near the horizon.
func AirmassPlaneParallelFromTrueAltitude(trueAltitude float64) float64 {
return formula.AirmassPlaneParallel(trueAltitude)
}
@@ -12,6 +13,7 @@ func AirmassPlaneParallelFromTrueAltitude(trueAltitude float64) float64 {
// AirmassKastenYoungFromApparentAltitude Kasten-Young 大气质量 / Kasten-Young airmass from apparent altitude.
//
// 输入为视高度角,单位度。
// Input is apparent altitude in degrees.
func AirmassKastenYoungFromApparentAltitude(apparentAltitude float64) float64 {
return formula.AirmassKastenYoung(apparentAltitude)
}
@@ -19,6 +21,7 @@ func AirmassKastenYoungFromApparentAltitude(apparentAltitude float64) float64 {
// AirmassPickeringFromApparentAltitude Pickering 大气质量 / Pickering airmass from apparent altitude.
//
// 输入为视高度角,单位度。
// Input is apparent altitude in degrees.
func AirmassPickeringFromApparentAltitude(apparentAltitude float64) float64 {
return formula.AirmassPickering(apparentAltitude)
}
@@ -26,6 +29,7 @@ func AirmassPickeringFromApparentAltitude(apparentAltitude float64) float64 {
// AirmassKastenYoungFromTrueAltitude Kasten-Young 大气质量 / Kasten-Young airmass from true altitude.
//
// 先用 pressureHPa / temperatureC 估算大气折射,将真高度角换算为视高度角,再代入经验公式。
// First estimates atmospheric refraction from pressureHPa and temperatureC, converts true altitude to apparent altitude, and then applies the empirical formula.
func AirmassKastenYoungFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
return formula.AirmassKastenYoung(ApparentAltitude(trueAltitude, pressureHPa, temperatureC))
}
@@ -33,6 +37,7 @@ func AirmassKastenYoungFromTrueAltitude(trueAltitude, pressureHPa, temperatureC
// AirmassPickeringFromTrueAltitude Pickering 大气质量 / Pickering airmass from true altitude.
//
// 先用 pressureHPa / temperatureC 估算大气折射,将真高度角换算为视高度角,再代入经验公式。
// First estimates atmospheric refraction from pressureHPa and temperatureC, converts true altitude to apparent altitude, and then applies the empirical formula.
func AirmassPickeringFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
return formula.AirmassPickering(ApparentAltitude(trueAltitude, pressureHPa, temperatureC))
}
+4
View File
@@ -9,6 +9,7 @@ import (
// AtmosphericRefractionFromTrueAltitude 真高度角折射修正 / atmospheric refraction from true altitude.
//
// 输入真高度角,返回应加到真高度角上的大气折射修正量,单位度。
// Takes true altitude and returns the atmospheric-refraction correction to be added to it, in degrees.
func AtmosphericRefractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
return basic.RefractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC)
}
@@ -16,6 +17,7 @@ func AtmosphericRefractionFromTrueAltitude(trueAltitude, pressureHPa, temperatur
// AtmosphericRefractionFromApparentAltitude 视高度角折射修正 / atmospheric refraction from apparent altitude.
//
// 输入视高度角,返回对应的大气折射修正量,单位度。
// Takes apparent altitude and returns the corresponding atmospheric-refraction correction, in degrees.
func AtmosphericRefractionFromApparentAltitude(apparentAltitude, pressureHPa, temperatureC float64) float64 {
return basic.RefractionFromApparentAltitude(apparentAltitude, pressureHPa, temperatureC)
}
@@ -23,6 +25,7 @@ func AtmosphericRefractionFromApparentAltitude(apparentAltitude, pressureHPa, te
// ApparentAltitude 真高度角转视高度角 / apparent altitude from true altitude.
//
// 输入真高度角,返回加入标准大气折射后的视高度角,单位度。
// Takes true altitude and returns the apparent altitude after applying standard atmospheric refraction, in degrees.
func ApparentAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
return basic.ApparentAltitude(trueAltitude, pressureHPa, temperatureC)
}
@@ -30,6 +33,7 @@ func ApparentAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
// TrueAltitude 视高度角转真高度角 / true altitude from apparent altitude.
//
// 输入视高度角,返回去除大气折射后的真高度角,单位度。
// Takes apparent altitude and returns the true altitude after removing atmospheric refraction, in degrees.
func TrueAltitude(apparentAltitude, pressureHPa, temperatureC float64) float64 {
return basic.TrueAltitude(apparentAltitude, pressureHPa, temperatureC)
}
+16
View File
@@ -25,6 +25,8 @@ var icrsToGalacticMatrix = [3][3]float64{
// 返回:
//
// 赤经 RA,单位度;赤纬 Dec,单位度
//
// Returns right ascension and declination in degrees for the supplied ecliptic longitude, latitude, and obliquity.
func EclipticToEquatorialByObliquity(lon, lat, obliquity float64) Equatorial {
sinLon, cosLon := sinCosDeg(lon)
sinLat, cosLat := sinCosDeg(lat)
@@ -44,6 +46,8 @@ func EclipticToEquatorialByObliquity(lon, lat, obliquity float64) Equatorial {
// 返回:
//
// 黄经 Lon,单位度;黄纬 Lat,单位度
//
// Returns ecliptic longitude and latitude in degrees for the supplied right ascension, declination, and obliquity.
func EquatorialToEclipticByObliquity(ra, dec, obliquity float64) Ecliptic {
sinRA, cosRA := sinCosDeg(ra)
sinDec, cosDec := sinCosDeg(dec)
@@ -63,6 +67,8 @@ func EquatorialToEclipticByObliquity(ra, dec, obliquity float64) Ecliptic {
// 返回:
//
// 方位角 Azimuth(正北为0,顺时针增加)、高度角 Altitude、天顶距 Zenith,均为度
//
// Returns azimuth, altitude, and zenith distance in degrees from the supplied hour angle, declination, and site latitude.
func HourAngleDeclinationToHorizontal(hourAngle, declination, latitude float64) Horizontal {
sinLatitude, cosLatitude := sinCosDeg(latitude)
sinDeclination, cosDeclination := sinCosDeg(declination)
@@ -87,6 +93,8 @@ func HourAngleDeclinationToHorizontal(hourAngle, declination, latitude float64)
// 返回:
//
// 时角 HourAngle,单位度;赤纬 Declination,单位度
//
// Returns hour angle and declination in degrees from the supplied horizontal coordinates and site latitude.
func HorizontalToHourAngleDeclination(azimuth, altitude, latitude float64) (hourAngle, declination float64) {
sinLatitude, cosLatitude := sinCosDeg(latitude)
sinAltitude, cosAltitude := sinCosDeg(altitude)
@@ -111,6 +119,8 @@ func HorizontalToHourAngleDeclination(azimuth, altitude, latitude float64) (hour
// 方位角 Azimuth(正北为0,顺时针增加)、高度角 Altitude、天顶距 Zenith,均为度;
// 附带返回对应的时角 HourAngle,单位度
//
// Returns horizontal coordinates in degrees from local sidereal time, right ascension, declination, and site latitude.
//
// 例:
//
// hz := coord.EquatorialToHorizontalByLocalSiderealTime(10.5, 83.6331, 22.0145, 31.2)
@@ -130,6 +140,8 @@ func EquatorialToHorizontalByLocalSiderealTime(localSiderealTimeHours, ra, dec,
//
// 赤经 RA,单位度;赤纬 Dec,单位度
//
// Returns right ascension and declination in degrees from local sidereal time and the supplied horizontal coordinates.
//
// 例:
//
// eq := coord.HorizontalToEquatorialByLocalSiderealTime(10.5, 128.2, 37.6, 31.2)
@@ -147,6 +159,8 @@ func HorizontalToEquatorialByLocalSiderealTime(localSiderealTimeHours, azimuth,
// 返回:
//
// 银经 Lon,单位度;银纬 Lat,单位度
//
// Returns galactic longitude and latitude in degrees from ICRS right ascension and declination.
func EquatorialToGalactic(ra, dec float64) Galactic {
vector := sphericalToVector(ra, dec)
rotated := matrixVectorMul(icrsToGalacticMatrix, vector)
@@ -162,6 +176,8 @@ func EquatorialToGalactic(ra, dec float64) Galactic {
// 返回:
//
// ICRS 赤经 RA,单位度;ICRS 赤纬 Dec,单位度
//
// Returns ICRS right ascension and declination in degrees from galactic longitude and latitude.
func GalacticToEquatorial(lon, lat float64) Equatorial {
vector := sphericalToVector(lon, lat)
rotated := matrixTransposeVectorMul(icrsToGalacticMatrix, vector)
+1 -1
View File
@@ -279,7 +279,7 @@ func searchLunarEclipse(
return lunarEclipseInfoFromBasic(result, date.Location()), true
}
}
candidateTT = basic.CalcMoonSHByJDE(candidateTT+float64(direction)*lunarEclipseSynodicMonthDays, 1)
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 1, direction, lunarEclipseSynodicMonthDays)
}
return LunarEclipseInfo{}, false
+72 -6
View File
@@ -147,6 +147,12 @@ func LastLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarE
return LastLocalLunarEclipseDanjon(date, lon, lat, height)
}
// LastLocalTotalLunarEclipse 上次可见月全食 / previous visible local total lunar eclipse.
// Previous visible local total lunar eclipse, using Danjon by default.
func LastLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) {
return searchLocalTotalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible)
}
// LastLocalLunarEclipseDanjon 上次可见月食(Danjon / previous visible local lunar eclipse with Danjon model.
// Previous visible local lunar eclipse with the Danjon model.
func LastLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo {
@@ -187,6 +193,12 @@ func NextLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarE
return NextLocalLunarEclipseDanjon(date, lon, lat, height)
}
// NextLocalTotalLunarEclipse 下次可见月全食 / next visible local total lunar eclipse.
// Next visible local total lunar eclipse, using Danjon by default.
func NextLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) {
return searchLocalTotalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible)
}
// NextLocalLunarEclipseDanjon 下次可见月食(Danjon / next visible local lunar eclipse with Danjon model.
// Next visible local lunar eclipse with the Danjon model.
func NextLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo {
@@ -227,6 +239,14 @@ func ClosestLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLun
return ClosestLocalLunarEclipseDanjon(date, lon, lat, height)
}
// ClosestLocalTotalLunarEclipse 最近一次可见月全食 / closest visible local total lunar eclipse.
// Closest visible local total lunar eclipse, using Danjon by default.
func ClosestLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) {
last, hasLast := searchLocalTotalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible)
next, hasNext := searchLocalTotalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible)
return closestLocalLunarEclipseResult(date, last, hasLast, next, hasNext)
}
// ClosestLocalLunarEclipseDanjon 最近一次可见月食(Danjon / closest visible local lunar eclipse with Danjon model.
// Closest visible local lunar eclipse with the Danjon model.
func ClosestLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo {
@@ -272,21 +292,32 @@ func closestLocalLunarEclipse(
next LocalLunarEclipseInfo,
hasNext bool,
) LocalLunarEclipseInfo {
info, _ := closestLocalLunarEclipseResult(date, last, hasLast, next, hasNext)
return info
}
func closestLocalLunarEclipseResult(
date time.Time,
last LocalLunarEclipseInfo,
hasLast bool,
next LocalLunarEclipseInfo,
hasNext bool,
) (LocalLunarEclipseInfo, bool) {
switch {
case hasLast && !hasNext:
return last
return last, true
case !hasLast && hasNext:
return next
return next, true
case !hasLast && !hasNext:
return LocalLunarEclipseInfo{}
return LocalLunarEclipseInfo{}, false
}
lastDistance := math.Abs(date.Sub(last.Maximum).Seconds())
nextDistance := math.Abs(next.Maximum.Sub(date).Seconds())
if lastDistance <= nextDistance {
return last
return last, true
}
return next
return next, true
}
func searchLocalLunarEclipse(
@@ -311,7 +342,35 @@ func searchLocalLunarEclipse(
}
}
}
candidateTT = basic.CalcMoonSHByJDE(candidateTT+float64(direction)*lunarEclipseSynodicMonthDays, 1)
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 1, direction, lunarEclipseSynodicMonthDays)
}
return LocalLunarEclipseInfo{}, false
}
func searchLocalTotalLunarEclipse(
date time.Time,
lon, lat, height float64,
direction int,
includeCurrent bool,
calculator lunarEclipseCalculator,
mode localLunarEclipseQueryMode,
) (LocalLunarEclipseInfo, bool) {
targetTT := timeToTTJDE(date)
candidateTT := basic.CalcMoonSHByJDE(targetTT, 1)
for i := 0; i < localLunarEclipseSearchLimit; i++ {
if isPotentialLunarEclipse(candidateTT) {
result := calculator(candidateTT)
if result.HasTotal {
info := localLunarEclipseInfoFromBasic(result, lon, lat, height, date.Location())
if (mode != localLunarEclipseQueryVisible || localTotalLunarEclipseVisible(info)) &&
lunarEclipseMatchesDirection(result.Maximum, targetTT, direction, includeCurrent) {
return info, true
}
}
}
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 1, direction, lunarEclipseSynodicMonthDays)
}
return LocalLunarEclipseInfo{}, false
@@ -375,6 +434,13 @@ func localLunarEclipseVisible(info LocalLunarEclipseInfo) bool {
return localLunarEclipseVisibleDuring(info, eventStart, eventEnd)
}
func localTotalLunarEclipseVisible(info LocalLunarEclipseInfo) bool {
if !info.HasTotal || info.TotalStart.IsZero() || info.TotalEnd.IsZero() {
return false
}
return localLunarEclipseVisibleDuring(info, info.TotalStart, info.TotalEnd)
}
func localLunarEclipseVisibleOnDate(info LocalLunarEclipseInfo, dayStart, dayEnd time.Time) bool {
eventStart, eventEnd, ok := localLunarEclipseRange(info)
if !ok {
+55
View File
@@ -133,6 +133,61 @@ func TestLocalLunarEclipseSearchBeyondFiveYears(t *testing.T) {
}
}
func TestLocalTotalLunarEclipseSearch(t *testing.T) {
loc := time.FixedZone("CDT", -5*3600)
lon, lat, height := -87.65, 41.85, 0.0
date := time.Date(2025, 3, 13, 0, 0, 0, 0, loc)
next, ok := NextLocalTotalLunarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find next local total lunar eclipse")
}
if next.Type != LunarEclipseTotal || !next.HasTotal {
t.Fatalf("unexpected next total lunar eclipse: %+v", next)
}
assertTimeClose(t, "NextLocalTotalLunarEclipse", next.Maximum, time.Date(2025, 3, 14, 1, 58, 47, 0, loc), 2*time.Minute)
last, ok := LastLocalTotalLunarEclipse(next.Maximum, lon, lat, height)
if !ok {
t.Fatal("expected to find previous local total lunar eclipse")
}
if last.Type != LunarEclipseTotal || !last.HasTotal {
t.Fatalf("unexpected last total lunar eclipse: %+v", last)
}
assertTimeClose(t, "LastLocalTotalLunarEclipse", last.Maximum, next.Maximum, time.Second)
}
func TestLocalTotalLunarEclipseClosest(t *testing.T) {
loc := time.FixedZone("CDT", -5*3600)
lon, lat, height := -87.65, 41.85, 0.0
date := time.Date(2025, 3, 14, 0, 0, 0, 0, loc)
info, ok := ClosestLocalTotalLunarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find closest local total lunar eclipse")
}
if info.Type != LunarEclipseTotal || !info.HasTotal {
t.Fatalf("unexpected closest total lunar eclipse: %+v", info)
}
assertTimeClose(t, "ClosestLocalTotalLunarEclipse", info.Maximum, time.Date(2025, 3, 14, 1, 58, 47, 0, loc), 2*time.Minute)
}
func TestLocalTotalLunarEclipseVisibleRequiresTotalPhaseVisibility(t *testing.T) {
info, ok := LocalLunarEclipseOnDate(time.Date(2025, 3, 14, 12, 0, 0, 0, time.UTC), -0.1278, 51.5074, 0)
if !ok {
t.Fatalf("expected visible local eclipse in London")
}
if info.Type != LunarEclipseTotal || !info.HasTotal {
t.Fatalf("unexpected eclipse type: %+v", info)
}
if !localLunarEclipseVisible(info) {
t.Fatalf("expected some phase to be visible")
}
if localTotalLunarEclipseVisible(info) {
t.Fatalf("expected total phase below horizon to be rejected")
}
}
func TestLocalLunarEclipseInfoKeepsLocation(t *testing.T) {
loc := time.FixedZone("JST", 9*3600)
lon, lat, height := 139.6917, 35.6895, 1234.0
+125 -5
View File
@@ -1,6 +1,7 @@
package eclipse
import (
"math"
"time"
"b612.me/astro/basic"
@@ -10,6 +11,18 @@ const (
sarosCycleLunations = 223
sarosCycleDays = float64(sarosCycleLunations) * solarEclipseSynodicMonthDays
sarosWalkLimit = 100
sarosMagicYearOffset = 3000
sarosMagicCountMask = 0x7f
sarosMagicDayMask = 0x1f
sarosMagicMonthMask = 0x0f
sarosMagicYearMask = 0x1fff
sarosMagicCountShift = 0
sarosMagicDayShift = 7
sarosMagicMonthShift = 12
sarosMagicYearShift = 16
sarosMagicMatchLimitDay = 12.0
sarosMagicTieEpsilonDay = 1e-9
)
// SarosInfo 沙罗序列信息, Saros series metadata.
@@ -25,6 +38,8 @@ type SarosInfo struct {
Count int
}
type sarosMagic uint32
type sarosAnchor struct {
Series int16
Count uint8
@@ -53,6 +68,20 @@ var lunarSarosHeadOverrides = [...]sarosHeadOverride{
}
func solarSarosInfo(ttJDE float64) (SarosInfo, bool) {
if info, ok := matchSarosMagic(solarSarosAnchors[:], 0, solarSarosHeadOverrides[:], ttJDE); ok {
return info, true
}
return solarSarosInfoByWalk(ttJDE)
}
func lunarSarosInfo(ttJDE float64) (SarosInfo, bool) {
if info, ok := matchSarosMagic(lunarSarosAnchors[:], 1, lunarSarosHeadOverrides[:], ttJDE); ok {
return info, true
}
return lunarSarosInfoByWalk(ttJDE)
}
func solarSarosInfoByWalk(ttJDE float64) (SarosInfo, bool) {
headTT, member, ok := solarSarosHead(ttJDE)
if !ok {
return SarosInfo{}, false
@@ -60,7 +89,7 @@ func solarSarosInfo(ttJDE float64) (SarosInfo, bool) {
if info, ok := matchSarosHeadOverride(solarSarosHeadOverrides[:], headTT, member); ok {
return info, true
}
anchor, ok := matchSarosAnchor(solarSarosAnchors[:], headTT)
anchor, ok := matchSarosAnchor(solarSarosAnchors[:], 0, headTT)
if !ok || member > int(anchor.Count) {
return SarosInfo{}, false
}
@@ -71,7 +100,7 @@ func solarSarosInfo(ttJDE float64) (SarosInfo, bool) {
}, true
}
func lunarSarosInfo(ttJDE float64) (SarosInfo, bool) {
func lunarSarosInfoByWalk(ttJDE float64) (SarosInfo, bool) {
headTT, member, ok := lunarSarosHead(ttJDE)
if !ok {
return SarosInfo{}, false
@@ -79,7 +108,7 @@ func lunarSarosInfo(ttJDE float64) (SarosInfo, bool) {
if info, ok := matchSarosHeadOverride(lunarSarosHeadOverrides[:], headTT, member); ok {
return info, true
}
anchor, ok := matchSarosAnchor(lunarSarosAnchors[:], headTT)
anchor, ok := matchSarosAnchor(lunarSarosAnchors[:], 1, headTT)
if !ok || member > int(anchor.Count) {
return SarosInfo{}, false
}
@@ -120,11 +149,102 @@ func lunarSarosHead(ttJDE float64) (float64, int, bool) {
return 0, 0, false
}
func matchSarosAnchor(anchors []sarosAnchor, headTT float64) (sarosAnchor, bool) {
func matchSarosMagic(anchors []sarosMagic, seriesBase int, overrides []sarosHeadOverride, ttJDE float64) (SarosInfo, bool) {
if info, ok := matchSarosMagicOverrides(overrides, ttJDE); ok {
return info, true
}
bestDistance := math.Inf(1)
best := SarosInfo{}
for index, magic := range anchors {
anchor := decodeSarosMagic(magic, seriesBase+index)
info, distance, ok := matchSarosMagicCandidate(ttJDE, anchor, 0)
if !ok {
continue
}
if betterSarosMagicMatch(info, distance, best, bestDistance) {
bestDistance = distance
best = info
}
}
if bestDistance <= sarosMagicMatchLimitDay {
return best, true
}
return SarosInfo{}, false
}
func matchSarosMagicOverrides(overrides []sarosHeadOverride, ttJDE float64) (SarosInfo, bool) {
bestDistance := math.Inf(1)
best := SarosInfo{}
for _, override := range overrides {
anchor := sarosAnchor{
Series: override.Series,
Count: override.Count,
Year: override.HeadYear,
Month: override.HeadMonth,
Day: override.HeadDay,
}
info, distance, ok := matchSarosMagicCandidate(ttJDE, anchor, int(override.MemberOffset))
if !ok {
continue
}
if betterSarosMagicMatch(info, distance, best, bestDistance) {
bestDistance = distance
best = info
}
}
if bestDistance <= sarosMagicMatchLimitDay {
return best, true
}
return SarosInfo{}, false
}
func matchSarosMagicCandidate(ttJDE float64, anchor sarosAnchor, memberOffset int) (SarosInfo, float64, bool) {
headTT := basic.JDECalc(int(anchor.Year), int(anchor.Month), float64(anchor.Day))
if math.IsNaN(headTT) {
return SarosInfo{}, 0, false
}
member := int(math.Round((ttJDE-headTT)/sarosCycleDays)) + 1 + memberOffset
if member < 1 || member > int(anchor.Count) {
return SarosInfo{}, 0, false
}
expectedTT := headTT + float64(member-1-memberOffset)*sarosCycleDays
return SarosInfo{
Series: int(anchor.Series),
Member: member,
Count: int(anchor.Count),
}, math.Abs(ttJDE - expectedTT), true
}
func betterSarosMagicMatch(info SarosInfo, distance float64, best SarosInfo, bestDistance float64) bool {
if distance < bestDistance-sarosMagicTieEpsilonDay {
return true
}
if math.Abs(distance-bestDistance) > sarosMagicTieEpsilonDay {
return false
}
if info.Series != best.Series {
return info.Series < best.Series
}
return info.Member < best.Member
}
func decodeSarosMagic(magic sarosMagic, series int) sarosAnchor {
value := uint32(magic)
return sarosAnchor{
Series: int16(series),
Count: uint8((value >> sarosMagicCountShift) & sarosMagicCountMask),
Year: int16(int((value>>sarosMagicYearShift)&sarosMagicYearMask) - sarosMagicYearOffset),
Month: uint8((value >> sarosMagicMonthShift) & sarosMagicMonthMask),
Day: uint8((value >> sarosMagicDayShift) & sarosMagicDayMask),
}
}
func matchSarosAnchor(anchors []sarosMagic, seriesBase int, headTT float64) (sarosAnchor, bool) {
headDate := basic.JDE2DateByZone(headTT, time.UTC, true)
year, month, day := headDate.Date()
monthNumber := int(month)
for _, anchor := range anchors {
for index, magic := range anchors {
anchor := decodeSarosMagic(magic, seriesBase+index)
if int(anchor.Year) == year && int(anchor.Month) == monthNumber && int(anchor.Day) == day {
return anchor, true
}
+181 -181
View File
@@ -2,185 +2,185 @@ package eclipse
// Code generated by /tmp/generate_saros_tables.go; DO NOT EDIT.
var lunarSarosAnchors = [...]sarosAnchor{
{Series: 1, Count: 73, Year: -2570, Month: 3, Day: 14},
{Series: 2, Count: 73, Year: -2523, Month: 3, Day: 3},
{Series: 3, Count: 76, Year: -2567, Month: 12, Day: 30},
{Series: 4, Count: 78, Year: -2646, Month: 10, Day: 6},
{Series: 5, Count: 77, Year: -2455, Month: 12, Day: 22},
{Series: 6, Count: 86, Year: -2624, Month: 8, Day: 4},
{Series: 7, Count: 89, Year: -2595, Month: 7, Day: 16},
{Series: 8, Count: 86, Year: -2494, Month: 8, Day: 8},
{Series: 9, Count: 75, Year: -2501, Month: 6, Day: 26},
{Series: 10, Count: 74, Year: -2454, Month: 6, Day: 17},
{Series: 11, Count: 74, Year: -2371, Month: 6, Day: 29},
{Series: 12, Count: 73, Year: -2360, Month: 5, Day: 28},
{Series: 13, Count: 73, Year: -2313, Month: 5, Day: 20},
{Series: 14, Count: 73, Year: -2230, Month: 6, Day: 1},
{Series: 15, Count: 73, Year: -2219, Month: 4, Day: 30},
{Series: 16, Count: 73, Year: -2172, Month: 4, Day: 21},
{Series: 17, Count: 72, Year: -2089, Month: 5, Day: 4},
{Series: 18, Count: 73, Year: -2078, Month: 4, Day: 2},
{Series: 19, Count: 73, Year: -2031, Month: 3, Day: 24},
{Series: 20, Count: 72, Year: -1948, Month: 4, Day: 5},
{Series: 21, Count: 74, Year: -1955, Month: 2, Day: 22},
{Series: 22, Count: 74, Year: -1926, Month: 2, Day: 2},
{Series: 23, Count: 73, Year: -1825, Month: 2, Day: 25},
{Series: 24, Count: 85, Year: -2031, Month: 9, Day: 16},
{Series: 25, Count: 87, Year: -2038, Month: 8, Day: 6},
{Series: 26, Count: 85, Year: -1919, Month: 9, Day: 9},
{Series: 27, Count: 85, Year: -1926, Month: 7, Day: 28},
{Series: 28, Count: 74, Year: -1897, Month: 7, Day: 9},
{Series: 29, Count: 83, Year: -1814, Month: 7, Day: 21},
{Series: 30, Count: 74, Year: -1803, Month: 6, Day: 19},
{Series: 31, Count: 73, Year: -1774, Month: 5, Day: 30},
{Series: 32, Count: 73, Year: -1673, Month: 6, Day: 23},
{Series: 33, Count: 73, Year: -1662, Month: 5, Day: 22},
{Series: 34, Count: 72, Year: -1615, Month: 5, Day: 13},
{Series: 35, Count: 72, Year: -1532, Month: 5, Day: 25},
{Series: 36, Count: 73, Year: -1521, Month: 4, Day: 24},
{Series: 37, Count: 72, Year: -1492, Month: 4, Day: 3},
{Series: 38, Count: 72, Year: -1391, Month: 4, Day: 27},
{Series: 39, Count: 73, Year: -1380, Month: 3, Day: 26},
{Series: 40, Count: 73, Year: -1369, Month: 2, Day: 24},
{Series: 41, Count: 73, Year: -1268, Month: 3, Day: 18},
{Series: 42, Count: 74, Year: -1275, Month: 2, Day: 4},
{Series: 43, Count: 85, Year: -1463, Month: 9, Day: 7},
{Series: 44, Count: 76, Year: -1199, Month: 1, Day: 6},
{Series: 45, Count: 85, Year: -1351, Month: 8, Day: 29},
{Series: 46, Count: 76, Year: -1358, Month: 7, Day: 19},
{Series: 47, Count: 86, Year: -1275, Month: 7, Day: 31},
{Series: 48, Count: 75, Year: -1228, Month: 7, Day: 21},
{Series: 49, Count: 73, Year: -1217, Month: 6, Day: 21},
{Series: 50, Count: 73, Year: -1134, Month: 7, Day: 3},
{Series: 51, Count: 73, Year: -1105, Month: 6, Day: 13},
{Series: 52, Count: 72, Year: -1076, Month: 5, Day: 23},
{Series: 53, Count: 72, Year: -993, Month: 6, Day: 5},
{Series: 54, Count: 72, Year: -946, Month: 5, Day: 26},
{Series: 55, Count: 72, Year: -935, Month: 4, Day: 25},
{Series: 56, Count: 72, Year: -852, Month: 5, Day: 7},
{Series: 57, Count: 73, Year: -823, Month: 4, Day: 16},
{Series: 58, Count: 73, Year: -812, Month: 3, Day: 16},
{Series: 59, Count: 71, Year: -711, Month: 4, Day: 9},
{Series: 60, Count: 73, Year: -700, Month: 3, Day: 8},
{Series: 61, Count: 78, Year: -780, Month: 12, Day: 13},
{Series: 62, Count: 74, Year: -624, Month: 2, Day: 8},
{Series: 63, Count: 82, Year: -722, Month: 11, Day: 3},
{Series: 64, Count: 84, Year: -783, Month: 8, Day: 20},
{Series: 65, Count: 86, Year: -736, Month: 8, Day: 11},
{Series: 66, Count: 84, Year: -671, Month: 8, Day: 12},
{Series: 67, Count: 73, Year: -660, Month: 7, Day: 11},
{Series: 68, Count: 72, Year: -595, Month: 7, Day: 14},
{Series: 69, Count: 73, Year: -530, Month: 7, Day: 15},
{Series: 70, Count: 72, Year: -519, Month: 6, Day: 13},
{Series: 71, Count: 72, Year: -472, Month: 6, Day: 4},
{Series: 72, Count: 72, Year: -389, Month: 6, Day: 17},
{Series: 73, Count: 72, Year: -378, Month: 5, Day: 16},
{Series: 74, Count: 72, Year: -331, Month: 5, Day: 7},
{Series: 75, Count: 72, Year: -266, Month: 5, Day: 8},
{Series: 76, Count: 73, Year: -255, Month: 4, Day: 7},
{Series: 77, Count: 72, Year: -190, Month: 4, Day: 9},
{Series: 78, Count: 72, Year: -125, Month: 4, Day: 10},
{Series: 79, Count: 73, Year: -132, Month: 2, Day: 27},
{Series: 80, Count: 74, Year: -103, Month: 2, Day: 7},
{Series: 81, Count: 74, Year: -20, Month: 2, Day: 19},
{Series: 82, Count: 84, Year: -208, Month: 9, Day: 21},
{Series: 83, Count: 84, Year: -197, Month: 8, Day: 22},
{Series: 84, Count: 84, Year: -96, Month: 9, Day: 13},
{Series: 85, Count: 76, Year: -103, Month: 8, Day: 2},
{Series: 86, Count: 73, Year: -74, Month: 7, Day: 13},
{Series: 87, Count: 73, Year: 27, Month: 8, Day: 6},
{Series: 88, Count: 72, Year: 38, Month: 7, Day: 5},
{Series: 89, Count: 72, Year: 67, Month: 6, Day: 15},
{Series: 90, Count: 72, Year: 150, Month: 6, Day: 27},
{Series: 91, Count: 72, Year: 179, Month: 6, Day: 7},
{Series: 92, Count: 71, Year: 208, Month: 5, Day: 17},
{Series: 93, Count: 71, Year: 291, Month: 5, Day: 30},
{Series: 94, Count: 71, Year: 320, Month: 5, Day: 9},
{Series: 95, Count: 71, Year: 349, Month: 4, Day: 19},
{Series: 96, Count: 71, Year: 432, Month: 5, Day: 1},
{Series: 97, Count: 72, Year: 443, Month: 3, Day: 31},
{Series: 98, Count: 74, Year: 436, Month: 2, Day: 18},
{Series: 99, Count: 72, Year: 555, Month: 3, Day: 24},
{Series: 100, Count: 79, Year: 439, Month: 12, Day: 6},
{Series: 101, Count: 83, Year: 360, Month: 9, Day: 11},
{Series: 102, Count: 84, Year: 461, Month: 10, Day: 5},
{Series: 103, Count: 82, Year: 472, Month: 9, Day: 3},
{Series: 104, Count: 72, Year: 483, Month: 8, Day: 4},
{Series: 105, Count: 73, Year: 566, Month: 8, Day: 16},
{Series: 106, Count: 73, Year: 595, Month: 7, Day: 27},
{Series: 107, Count: 72, Year: 606, Month: 6, Day: 26},
{Series: 108, Count: 72, Year: 689, Month: 7, Day: 8},
{Series: 109, Count: 71, Year: 736, Month: 6, Day: 27},
{Series: 110, Count: 72, Year: 747, Month: 5, Day: 28},
{Series: 111, Count: 71, Year: 830, Month: 6, Day: 10},
{Series: 112, Count: 72, Year: 859, Month: 5, Day: 20},
{Series: 113, Count: 71, Year: 888, Month: 4, Day: 29},
{Series: 114, Count: 71, Year: 971, Month: 5, Day: 13},
{Series: 115, Count: 72, Year: 1000, Month: 4, Day: 21},
{Series: 116, Count: 73, Year: 993, Month: 3, Day: 11},
{Series: 117, Count: 71, Year: 1094, Month: 4, Day: 3},
{Series: 118, Count: 73, Year: 1105, Month: 3, Day: 2},
{Series: 119, Count: 82, Year: 935, Month: 10, Day: 14},
{Series: 120, Count: 83, Year: 1000, Month: 10, Day: 16},
{Series: 121, Count: 82, Year: 1047, Month: 10, Day: 6},
{Series: 122, Count: 74, Year: 1022, Month: 8, Day: 14},
{Series: 123, Count: 72, Year: 1087, Month: 8, Day: 16},
{Series: 124, Count: 73, Year: 1152, Month: 8, Day: 17},
{Series: 125, Count: 72, Year: 1163, Month: 7, Day: 17},
{Series: 126, Count: 70, Year: 1228, Month: 7, Day: 18},
{Series: 127, Count: 72, Year: 1275, Month: 7, Day: 9},
{Series: 128, Count: 71, Year: 1304, Month: 6, Day: 18},
{Series: 129, Count: 71, Year: 1351, Month: 6, Day: 10},
{Series: 130, Count: 71, Year: 1416, Month: 6, Day: 10},
{Series: 131, Count: 72, Year: 1427, Month: 5, Day: 10},
{Series: 132, Count: 71, Year: 1492, Month: 5, Day: 12},
{Series: 133, Count: 71, Year: 1557, Month: 5, Day: 13},
{Series: 134, Count: 72, Year: 1550, Month: 4, Day: 1},
{Series: 135, Count: 71, Year: 1615, Month: 4, Day: 13},
{Series: 136, Count: 72, Year: 1680, Month: 4, Day: 13},
{Series: 137, Count: 78, Year: 1564, Month: 12, Day: 17},
{Series: 138, Count: 82, Year: 1521, Month: 10, Day: 15},
{Series: 139, Count: 79, Year: 1658, Month: 12, Day: 9},
{Series: 140, Count: 77, Year: 1597, Month: 9, Day: 25},
{Series: 141, Count: 72, Year: 1608, Month: 8, Day: 25},
{Series: 142, Count: 73, Year: 1709, Month: 9, Day: 19},
{Series: 143, Count: 72, Year: 1720, Month: 8, Day: 18},
{Series: 144, Count: 71, Year: 1749, Month: 7, Day: 29},
{Series: 145, Count: 71, Year: 1832, Month: 8, Day: 11},
{Series: 146, Count: 72, Year: 1843, Month: 7, Day: 11},
{Series: 147, Count: 70, Year: 1890, Month: 7, Day: 2},
{Series: 148, Count: 70, Year: 1973, Month: 7, Day: 15},
{Series: 149, Count: 71, Year: 1984, Month: 6, Day: 13},
{Series: 150, Count: 71, Year: 2013, Month: 5, Day: 25},
{Series: 151, Count: 71, Year: 2096, Month: 6, Day: 6},
{Series: 152, Count: 72, Year: 2107, Month: 5, Day: 7},
{Series: 153, Count: 71, Year: 2136, Month: 4, Day: 16},
{Series: 154, Count: 71, Year: 2237, Month: 5, Day: 10},
{Series: 155, Count: 73, Year: 2212, Month: 3, Day: 18},
{Series: 156, Count: 81, Year: 2060, Month: 11, Day: 8},
{Series: 157, Count: 73, Year: 2306, Month: 3, Day: 1},
{Series: 158, Count: 81, Year: 2154, Month: 10, Day: 21},
{Series: 159, Count: 73, Year: 2147, Month: 9, Day: 9},
{Series: 160, Count: 72, Year: 2248, Month: 10, Day: 3},
{Series: 161, Count: 73, Year: 2259, Month: 9, Day: 2},
{Series: 162, Count: 71, Year: 2288, Month: 8, Day: 12},
{Series: 163, Count: 70, Year: 2371, Month: 8, Day: 27},
{Series: 164, Count: 71, Year: 2400, Month: 8, Day: 5},
{Series: 165, Count: 71, Year: 2411, Month: 7, Day: 6},
{Series: 166, Count: 70, Year: 2494, Month: 7, Day: 18},
{Series: 167, Count: 71, Year: 2541, Month: 7, Day: 9},
{Series: 168, Count: 71, Year: 2552, Month: 6, Day: 8},
{Series: 169, Count: 70, Year: 2635, Month: 6, Day: 22},
{Series: 170, Count: 71, Year: 2664, Month: 6, Day: 1},
{Series: 171, Count: 71, Year: 2675, Month: 5, Day: 1},
{Series: 172, Count: 70, Year: 2758, Month: 5, Day: 15},
{Series: 173, Count: 72, Year: 2787, Month: 4, Day: 24},
{Series: 174, Count: 79, Year: 2635, Month: 12, Day: 16},
{Series: 175, Count: 74, Year: 2791, Month: 2, Day: 11},
{Series: 176, Count: 79, Year: 2747, Month: 12, Day: 9},
{Series: 177, Count: 73, Year: 2704, Month: 10, Day: 5},
{Series: 178, Count: 70, Year: 2769, Month: 10, Day: 7},
{Series: 179, Count: 73, Year: 2816, Month: 9, Day: 27},
{Series: 180, Count: 71, Year: 2827, Month: 8, Day: 28},
var lunarSarosAnchors = [...]sarosMagic{
0x21ae3749,
0x21dd31c9,
0x21b1cf4c,
0x2162a34e,
0x2221cb4d,
0x21788256,
0x21957859,
0x21fa8456,
0x21f36d4b,
0x222268ca,
0x22756eca,
0x22805e49,
0x22af5a49,
0x230260c9,
0x230d4f49,
0x233c4ac9,
0x238f5248,
0x239a4149,
0x23c93c49,
0x241c42c8,
0x24152b4a,
0x2432214a,
0x24972cc9,
0x23c99855,
0x23c28357,
0x243994d5,
0x24327e55,
0x244f74ca,
0x24a27ad3,
0x24ad69ca,
0x24ca5f49,
0x252f6bc9,
0x253a5b49,
0x256956c8,
0x25bc5cc8,
0x25c74c49,
0x25e441c8,
0x26494dc8,
0x26543d49,
0x265f2c49,
0x26c43949,
0x26bd224a,
0x260193d5,
0x2709134c,
0x26718ed5,
0x266a79cc,
0x26bd7fd6,
0x26ec7acb,
0x26f76ac9,
0x274a71c9,
0x276766c9,
0x27845bc8,
0x27d762c8,
0x28065d48,
0x28114cc8,
0x286453c8,
0x28814849,
0x288c3849,
0x28f144c7,
0x28fc3449,
0x28acc6ce,
0x2948244a,
0x28e6b1d2,
0x28a98a54,
0x28d885d6,
0x29198654,
0x292475c9,
0x29657748,
0x29a677c9,
0x29b166c8,
0x29e06248,
0x2a3368c8,
0x2a3e5848,
0x2a6d53c8,
0x2aae5448,
0x2ab943c9,
0x2afa44c8,
0x2b3b4548,
0x2b342dc9,
0x2b5123ca,
0x2ba429ca,
0x2ae89ad4,
0x2af38b54,
0x2b5896d4,
0x2b51814c,
0x2b6e76c9,
0x2bd38349,
0x2bde72c8,
0x2bfb67c8,
0x2c4e6dc8,
0x2c6b63c8,
0x2c8858c7,
0x2cdb5f47,
0x2cf854c7,
0x2d1549c7,
0x2d6850c7,
0x2d733fc8,
0x2d6c294a,
0x2de33c48,
0x2d6fc34f,
0x2d2095d3,
0x2d85a2d4,
0x2d9091d2,
0x2d9b8248,
0x2dee8849,
0x2e0b7dc9,
0x2e166d48,
0x2e697448,
0x2e986dc7,
0x2ea35e48,
0x2ef66547,
0x2f135a48,
0x2f304ec7,
0x2f8356c7,
0x2fa04ac8,
0x2f9935c9,
0x2ffe41c7,
0x30093149,
0x2f5fa752,
0x2fa0a853,
0x2fcfa352,
0x2fb6874a,
0x2ff78848,
0x303888c9,
0x304378c8,
0x30847946,
0x30b374c8,
0x30d06947,
0x30ff6547,
0x31406547,
0x314b5548,
0x318c5647,
0x31cd56c7,
0x31c640c8,
0x320746c7,
0x324846c8,
0x31d4c8ce,
0x31a9a7d2,
0x3232c4cf,
0x31f59ccd,
0x32008cc8,
0x326599c9,
0x32708948,
0x328d7ec7,
0x32e085c7,
0x32eb75c8,
0x331a7146,
0x336d77c6,
0x337866c7,
0x33955cc7,
0x33e86347,
0x33f353c8,
0x34104847,
0x34755547,
0x345c3949,
0x33c4b451,
0x34ba30c9,
0x3422aad1,
0x341b94c9,
0x3480a1c8,
0x348b9149,
0x34a88647,
0x34fb8dc6,
0x351882c7,
0x35237347,
0x35767946,
0x35a574c7,
0x35b06447,
0x36036b46,
0x362060c7,
0x362b50c7,
0x367e57c6,
0x369b4c48,
0x3603c84f,
0x369f25ca,
0x3673c4cf,
0x3648a2c9,
0x3689a3c6,
0x36b89dc9,
0x36c38e47,
}
+182 -182
View File
@@ -2,186 +2,186 @@ package eclipse
// Code generated by /tmp/generate_saros_tables.go; DO NOT EDIT.
var solarSarosAnchors = [...]sarosAnchor{
{Series: 0, Count: 72, Year: -2955, Month: 5, Day: 23},
{Series: 1, Count: 72, Year: -2872, Month: 6, Day: 4},
{Series: 2, Count: 73, Year: -2861, Month: 5, Day: 4},
{Series: 3, Count: 72, Year: -2814, Month: 4, Day: 24},
{Series: 4, Count: 72, Year: -2731, Month: 5, Day: 6},
{Series: 5, Count: 73, Year: -2720, Month: 4, Day: 4},
{Series: 6, Count: 72, Year: -2673, Month: 3, Day: 27},
{Series: 7, Count: 72, Year: -2590, Month: 4, Day: 8},
{Series: 8, Count: 73, Year: -2579, Month: 3, Day: 7},
{Series: 9, Count: 74, Year: -2568, Month: 2, Day: 6},
{Series: 10, Count: 73, Year: -2467, Month: 2, Day: 28},
{Series: 11, Count: 76, Year: -2492, Month: 1, Day: 6},
{Series: 12, Count: 86, Year: -2662, Month: 8, Day: 20},
{Series: 13, Count: 85, Year: -2543, Month: 9, Day: 23},
{Series: 14, Count: 85, Year: -2550, Month: 8, Day: 11},
{Series: 15, Count: 75, Year: -2557, Month: 7, Day: 1},
{Series: 16, Count: 85, Year: -2456, Month: 7, Day: 23},
{Series: 17, Count: 74, Year: -2427, Month: 7, Day: 3},
{Series: 18, Count: 73, Year: -2416, Month: 6, Day: 2},
{Series: 19, Count: 73, Year: -2333, Month: 6, Day: 15},
{Series: 20, Count: 72, Year: -2286, Month: 6, Day: 5},
{Series: 21, Count: 72, Year: -2275, Month: 5, Day: 5},
{Series: 22, Count: 71, Year: -2174, Month: 5, Day: 28},
{Series: 23, Count: 72, Year: -2145, Month: 5, Day: 7},
{Series: 24, Count: 72, Year: -2134, Month: 4, Day: 6},
{Series: 25, Count: 71, Year: -2033, Month: 4, Day: 30},
{Series: 26, Count: 72, Year: -2004, Month: 4, Day: 8},
{Series: 27, Count: 72, Year: -1993, Month: 3, Day: 9},
{Series: 28, Count: 72, Year: -1910, Month: 3, Day: 22},
{Series: 29, Count: 73, Year: -1881, Month: 3, Day: 1},
{Series: 30, Count: 83, Year: -2051, Month: 10, Day: 12},
{Series: 31, Count: 74, Year: -1805, Month: 1, Day: 31},
{Series: 32, Count: 84, Year: -1957, Month: 9, Day: 24},
{Series: 33, Count: 84, Year: -1982, Month: 8, Day: 2},
{Series: 34, Count: 86, Year: -1917, Month: 8, Day: 4},
{Series: 35, Count: 84, Year: -1870, Month: 7, Day: 25},
{Series: 36, Count: 73, Year: -1859, Month: 6, Day: 23},
{Series: 37, Count: 73, Year: -1794, Month: 6, Day: 25},
{Series: 38, Count: 73, Year: -1729, Month: 6, Day: 26},
{Series: 39, Count: 72, Year: -1718, Month: 5, Day: 26},
{Series: 40, Count: 72, Year: -1653, Month: 5, Day: 28},
{Series: 41, Count: 72, Year: -1588, Month: 5, Day: 28},
{Series: 42, Count: 72, Year: -1577, Month: 4, Day: 28},
{Series: 43, Count: 72, Year: -1512, Month: 4, Day: 29},
{Series: 44, Count: 72, Year: -1447, Month: 4, Day: 30},
{Series: 45, Count: 72, Year: -1436, Month: 3, Day: 30},
{Series: 46, Count: 72, Year: -1371, Month: 4, Day: 1},
{Series: 47, Count: 72, Year: -1306, Month: 4, Day: 2},
{Series: 48, Count: 74, Year: -1331, Month: 2, Day: 8},
{Series: 49, Count: 72, Year: -1248, Month: 2, Day: 22},
{Series: 50, Count: 73, Year: -1201, Month: 2, Day: 11},
{Series: 51, Count: 85, Year: -1407, Month: 9, Day: 2},
{Series: 52, Count: 86, Year: -1378, Month: 8, Day: 14},
{Series: 53, Count: 84, Year: -1277, Month: 9, Day: 6},
{Series: 54, Count: 74, Year: -1284, Month: 7, Day: 25},
{Series: 55, Count: 73, Year: -1255, Month: 7, Day: 6},
{Series: 56, Count: 74, Year: -1172, Month: 7, Day: 17},
{Series: 57, Count: 73, Year: -1161, Month: 6, Day: 17},
{Series: 58, Count: 72, Year: -1114, Month: 6, Day: 7},
{Series: 59, Count: 72, Year: -1031, Month: 6, Day: 19},
{Series: 60, Count: 72, Year: -1020, Month: 5, Day: 18},
{Series: 61, Count: 71, Year: -973, Month: 5, Day: 10},
{Series: 62, Count: 71, Year: -890, Month: 5, Day: 22},
{Series: 63, Count: 72, Year: -879, Month: 4, Day: 20},
{Series: 64, Count: 71, Year: -832, Month: 4, Day: 11},
{Series: 65, Count: 71, Year: -749, Month: 4, Day: 24},
{Series: 66, Count: 73, Year: -756, Month: 3, Day: 12},
{Series: 67, Count: 72, Year: -709, Month: 3, Day: 4},
{Series: 68, Count: 72, Year: -626, Month: 3, Day: 16},
{Series: 69, Count: 78, Year: -724, Month: 12, Day: 9},
{Series: 70, Count: 84, Year: -821, Month: 9, Day: 5},
{Series: 71, Count: 82, Year: -684, Month: 10, Day: 19},
{Series: 72, Count: 83, Year: -727, Month: 8, Day: 16},
{Series: 73, Count: 72, Year: -698, Month: 7, Day: 27},
{Series: 74, Count: 75, Year: -615, Month: 8, Day: 8},
{Series: 75, Count: 73, Year: -604, Month: 7, Day: 7},
{Series: 76, Count: 72, Year: -575, Month: 6, Day: 18},
{Series: 77, Count: 71, Year: -474, Month: 7, Day: 11},
{Series: 78, Count: 72, Year: -463, Month: 6, Day: 9},
{Series: 79, Count: 71, Year: -434, Month: 5, Day: 21},
{Series: 80, Count: 71, Year: -333, Month: 6, Day: 13},
{Series: 81, Count: 72, Year: -322, Month: 5, Day: 12},
{Series: 82, Count: 71, Year: -293, Month: 4, Day: 22},
{Series: 83, Count: 71, Year: -210, Month: 5, Day: 5},
{Series: 84, Count: 72, Year: -181, Month: 4, Day: 14},
{Series: 85, Count: 72, Year: -170, Month: 3, Day: 14},
{Series: 86, Count: 71, Year: -69, Month: 4, Day: 6},
{Series: 87, Count: 73, Year: -76, Month: 2, Day: 23},
{Series: 88, Count: 83, Year: -246, Month: 10, Day: 6},
{Series: 89, Count: 73, Year: 18, Month: 2, Day: 4},
{Series: 90, Count: 83, Year: -134, Month: 9, Day: 28},
{Series: 91, Count: 75, Year: -159, Month: 8, Day: 6},
{Series: 92, Count: 74, Year: -76, Month: 8, Day: 19},
{Series: 93, Count: 74, Year: -29, Month: 8, Day: 9},
{Series: 94, Count: 72, Year: -18, Month: 7, Day: 9},
{Series: 95, Count: 71, Year: 47, Month: 7, Day: 11},
{Series: 96, Count: 72, Year: 94, Month: 7, Day: 1},
{Series: 97, Count: 71, Year: 123, Month: 6, Day: 11},
{Series: 98, Count: 71, Year: 188, Month: 6, Day: 12},
{Series: 99, Count: 72, Year: 235, Month: 6, Day: 3},
{Series: 100, Count: 71, Year: 264, Month: 5, Day: 13},
{Series: 101, Count: 71, Year: 329, Month: 5, Day: 15},
{Series: 102, Count: 71, Year: 376, Month: 5, Day: 5},
{Series: 103, Count: 72, Year: 387, Month: 4, Day: 4},
{Series: 104, Count: 70, Year: 470, Month: 4, Day: 17},
{Series: 105, Count: 72, Year: 499, Month: 3, Day: 27},
{Series: 106, Count: 75, Year: 456, Month: 1, Day: 23},
{Series: 107, Count: 72, Year: 557, Month: 2, Day: 15},
{Series: 108, Count: 76, Year: 550, Month: 1, Day: 4},
{Series: 109, Count: 81, Year: 416, Month: 9, Day: 7},
{Series: 110, Count: 72, Year: 463, Month: 8, Day: 30},
{Series: 111, Count: 79, Year: 528, Month: 8, Day: 30},
{Series: 112, Count: 72, Year: 539, Month: 7, Day: 31},
{Series: 113, Count: 71, Year: 586, Month: 7, Day: 22},
{Series: 114, Count: 72, Year: 651, Month: 7, Day: 23},
{Series: 115, Count: 72, Year: 662, Month: 6, Day: 21},
{Series: 116, Count: 70, Year: 727, Month: 6, Day: 23},
{Series: 117, Count: 71, Year: 792, Month: 6, Day: 24},
{Series: 118, Count: 72, Year: 803, Month: 5, Day: 24},
{Series: 119, Count: 71, Year: 850, Month: 5, Day: 15},
{Series: 120, Count: 71, Year: 933, Month: 5, Day: 27},
{Series: 121, Count: 71, Year: 944, Month: 4, Day: 25},
{Series: 122, Count: 70, Year: 991, Month: 4, Day: 17},
{Series: 123, Count: 70, Year: 1074, Month: 4, Day: 29},
{Series: 124, Count: 73, Year: 1049, Month: 3, Day: 6},
{Series: 125, Count: 73, Year: 1060, Month: 2, Day: 4},
{Series: 126, Count: 72, Year: 1179, Month: 3, Day: 10},
{Series: 127, Count: 82, Year: 991, Month: 10, Day: 10},
{Series: 128, Count: 73, Year: 984, Month: 8, Day: 29},
{Series: 129, Count: 80, Year: 1103, Month: 10, Day: 3},
{Series: 130, Count: 73, Year: 1096, Month: 8, Day: 20},
{Series: 131, Count: 70, Year: 1125, Month: 8, Day: 1},
{Series: 132, Count: 71, Year: 1208, Month: 8, Day: 13},
{Series: 133, Count: 72, Year: 1219, Month: 7, Day: 13},
{Series: 134, Count: 71, Year: 1248, Month: 6, Day: 22},
{Series: 135, Count: 71, Year: 1331, Month: 7, Day: 5},
{Series: 136, Count: 71, Year: 1360, Month: 6, Day: 14},
{Series: 137, Count: 70, Year: 1389, Month: 5, Day: 25},
{Series: 138, Count: 70, Year: 1472, Month: 6, Day: 6},
{Series: 139, Count: 71, Year: 1501, Month: 5, Day: 17},
{Series: 140, Count: 71, Year: 1512, Month: 4, Day: 16},
{Series: 141, Count: 70, Year: 1613, Month: 5, Day: 19},
{Series: 142, Count: 72, Year: 1624, Month: 4, Day: 17},
{Series: 143, Count: 72, Year: 1617, Month: 3, Day: 7},
{Series: 144, Count: 70, Year: 1736, Month: 4, Day: 11},
{Series: 145, Count: 77, Year: 1639, Month: 1, Day: 4},
{Series: 146, Count: 76, Year: 1541, Month: 9, Day: 19},
{Series: 147, Count: 80, Year: 1624, Month: 10, Day: 12},
{Series: 148, Count: 75, Year: 1653, Month: 9, Day: 21},
{Series: 149, Count: 71, Year: 1664, Month: 8, Day: 21},
{Series: 150, Count: 71, Year: 1729, Month: 8, Day: 24},
{Series: 151, Count: 72, Year: 1776, Month: 8, Day: 14},
{Series: 152, Count: 70, Year: 1805, Month: 7, Day: 26},
{Series: 153, Count: 70, Year: 1870, Month: 7, Day: 28},
{Series: 154, Count: 71, Year: 1917, Month: 7, Day: 19},
{Series: 155, Count: 71, Year: 1928, Month: 6, Day: 17},
{Series: 156, Count: 69, Year: 2011, Month: 7, Day: 1},
{Series: 157, Count: 70, Year: 2058, Month: 6, Day: 21},
{Series: 158, Count: 70, Year: 2069, Month: 5, Day: 20},
{Series: 159, Count: 70, Year: 2134, Month: 5, Day: 23},
{Series: 160, Count: 71, Year: 2181, Month: 5, Day: 13},
{Series: 161, Count: 72, Year: 2174, Month: 4, Day: 1},
{Series: 162, Count: 70, Year: 2257, Month: 4, Day: 15},
{Series: 163, Count: 72, Year: 2286, Month: 3, Day: 25},
{Series: 164, Count: 80, Year: 2098, Month: 10, Day: 24},
{Series: 165, Count: 72, Year: 2145, Month: 10, Day: 16},
{Series: 166, Count: 77, Year: 2228, Month: 10, Day: 29},
{Series: 167, Count: 72, Year: 2203, Month: 9, Day: 6},
{Series: 168, Count: 70, Year: 2250, Month: 8, Day: 28},
{Series: 169, Count: 71, Year: 2333, Month: 9, Day: 10},
{Series: 170, Count: 71, Year: 2344, Month: 8, Day: 9},
{Series: 171, Count: 69, Year: 2391, Month: 8, Day: 1},
{Series: 172, Count: 70, Year: 2474, Month: 8, Day: 13},
{Series: 173, Count: 70, Year: 2485, Month: 7, Day: 12},
{Series: 174, Count: 69, Year: 2532, Month: 7, Day: 4},
{Series: 175, Count: 70, Year: 2597, Month: 7, Day: 5},
{Series: 176, Count: 71, Year: 2608, Month: 6, Day: 4},
{Series: 177, Count: 69, Year: 2655, Month: 5, Day: 27},
{Series: 178, Count: 70, Year: 2738, Month: 6, Day: 9},
{Series: 179, Count: 71, Year: 2731, Month: 4, Day: 28},
{Series: 180, Count: 70, Year: 2760, Month: 4, Day: 8},
var solarSarosAnchors = [...]sarosMagic{
0x202d5bc8,
0x20806248,
0x208b5249,
0x20ba4c48,
0x210d5348,
0x21184249,
0x21473dc8,
0x219a4448,
0x21a533c9,
0x21b0234a,
0x22152e49,
0x21fc134c,
0x21528a56,
0x21c99bd5,
0x21c285d5,
0x21bb70cb,
0x22207bd5,
0x223d71ca,
0x22486149,
0x229b67c9,
0x22ca62c8,
0x22d552c8,
0x233a5e47,
0x235753c8,
0x23624348,
0x23c74f47,
0x23e44448,
0x23ef34c8,
0x24423b48,
0x245f30c9,
0x23b5a653,
0x24ab1fca,
0x24139c54,
0x23fa8154,
0x243b8256,
0x246a7cd4,
0x24756bc9,
0x24b66cc9,
0x24f76d49,
0x25025d48,
0x25435e48,
0x25845e48,
0x258f4e48,
0x25d04ec8,
0x26114f48,
0x261c3f48,
0x265d40c8,
0x269e4148,
0x2685244a,
0x26d82b48,
0x270725c9,
0x26399155,
0x26568756,
0x26bb9354,
0x26b47cca,
0x26d17349,
0x272478ca,
0x272f68c9,
0x275e63c8,
0x27b169c8,
0x27bc5948,
0x27eb5547,
0x283e5b47,
0x28494a48,
0x287845c7,
0x28cb4c47,
0x28c43649,
0x28f33248,
0x29463848,
0x28e4c4ce,
0x288392d4,
0x290ca9d2,
0x28e18853,
0x28fe7dc8,
0x2951844b,
0x295c73c9,
0x29796948,
0x29de75c7,
0x29e964c8,
0x2a065ac7,
0x2a6b66c7,
0x2a765648,
0x2a934b47,
0x2ae652c7,
0x2b034748,
0x2b0e3748,
0x2b734347,
0x2b6c2bc9,
0x2ac2a353,
0x2bca2249,
0x2b329e53,
0x2b19834b,
0x2b6c89ca,
0x2b9b84ca,
0x2ba674c8,
0x2be775c7,
0x2c1670c8,
0x2c3365c7,
0x2c746647,
0x2ca361c8,
0x2cc056c7,
0x2d0157c7,
0x2d3052c7,
0x2d3b4248,
0x2d8e48c6,
0x2dab3dc8,
0x2d801bcb,
0x2de527c8,
0x2dde124c,
0x2d5893d1,
0x2d878f48,
0x2dc88f4f,
0x2dd37fc8,
0x2e027b47,
0x2e437bc8,
0x2e4e6ac8,
0x2e8f6bc6,
0x2ed06c47,
0x2edb5c48,
0x2f0a57c7,
0x2f5d5dc7,
0x2f684cc7,
0x2f9748c6,
0x2fea4ec6,
0x2fd13349,
0x2fdc2249,
0x30533548,
0x2f97a552,
0x2f908ec9,
0x3007a1d0,
0x30008a49,
0x301d80c6,
0x307086c7,
0x307b76c8,
0x30986b47,
0x30eb72c7,
0x31086747,
0x31255cc6,
0x31786346,
0x319558c7,
0x31a04847,
0x320559c6,
0x321048c8,
0x320933c8,
0x328045c6,
0x321f124d,
0x31bd99cc,
0x3210a650,
0x322d9acb,
0x32388ac7,
0x32798c47,
0x32a88748,
0x32c57d46,
0x33067e46,
0x333579c7,
0x334068c7,
0x339370c5,
0x33c26ac6,
0x33cd5a46,
0x340e5bc6,
0x343d56c7,
0x343640c8,
0x348947c6,
0x34a63cc8,
0x33eaac50,
0x3419a848,
0x346caecd,
0x34539348,
0x34828e46,
0x34d59547,
0x34e084c7,
0x350f80c5,
0x356286c6,
0x356d7646,
0x359c7245,
0x35dd72c6,
0x35e86247,
0x36175dc5,
0x366a64c6,
0x36634e47,
0x36804446,
}
+13 -17
View File
@@ -117,10 +117,10 @@ func TestSolarPathAndFootprintsCarrySaros(t *testing.T) {
}
func TestSarosAnchorSanity(t *testing.T) {
assertSarosAnchorTable(t, solarSarosAnchors[:], true)
assertSarosAnchorTable(t, lunarSarosAnchors[:], false)
assertSarosHeadOverrides(t, solarSarosHeadOverrides[:], solarSarosAnchors[:])
assertSarosHeadOverrides(t, lunarSarosHeadOverrides[:], lunarSarosAnchors[:])
assertSarosAnchorTable(t, solarSarosAnchors[:], 0)
assertSarosAnchorTable(t, lunarSarosAnchors[:], 1)
assertSarosHeadOverrides(t, solarSarosHeadOverrides[:], solarSarosAnchors[:], 0)
assertSarosHeadOverrides(t, lunarSarosHeadOverrides[:], lunarSarosAnchors[:], 1)
}
func assertSarosInfo(t *testing.T, has bool, got SarosInfo, wantSeries, wantMember, wantCount int) {
@@ -141,14 +141,15 @@ func assertSarosInfo(t *testing.T, has bool, got SarosInfo, wantSeries, wantMemb
}
}
func assertSarosAnchorTable(t *testing.T, anchors []sarosAnchor, solar bool) {
func assertSarosAnchorTable(t *testing.T, anchors []sarosMagic, seriesBase int) {
t.Helper()
if len(anchors) == 0 {
t.Fatal("expected non-empty Saros anchor table")
}
seenDates := make(map[[3]int]int, len(anchors))
lastSeries := int(anchors[0].Series) - 1
for _, anchor := range anchors {
lastSeries := seriesBase - 1
for index, magic := range anchors {
anchor := decodeSarosMagic(magic, seriesBase+index)
series := int(anchor.Series)
if series <= lastSeries {
t.Fatalf("series not strictly increasing: prev=%d current=%d", lastSeries, series)
@@ -163,25 +164,20 @@ func assertSarosAnchorTable(t *testing.T, anchors []sarosAnchor, solar bool) {
}
seenDates[dateKey] = series
}
if solar {
if got := int(anchors[0].Series); got != 0 {
t.Fatalf("unexpected first solar series: got %d want 0", got)
}
} else {
if got := int(anchors[0].Series); got != 1 {
t.Fatalf("unexpected first lunar series: got %d want 1", got)
}
if got := int(decodeSarosMagic(anchors[0], seriesBase).Series); got != seriesBase {
t.Fatalf("unexpected first series: got %d want %d", got, seriesBase)
}
}
func assertSarosHeadOverrides(t *testing.T, overrides []sarosHeadOverride, anchors []sarosAnchor) {
func assertSarosHeadOverrides(t *testing.T, overrides []sarosHeadOverride, anchors []sarosMagic, seriesBase int) {
t.Helper()
if len(overrides) == 0 {
return
}
seenHeads := make(map[[3]int]int, len(overrides))
anchorSeries := make(map[int]int, len(anchors))
for _, anchor := range anchors {
for index, magic := range anchors {
anchor := decodeSarosMagic(magic, seriesBase+index)
anchorSeries[int(anchor.Series)] = int(anchor.Count)
}
for _, override := range overrides {
+36
View File
@@ -0,0 +1,36 @@
package eclipse
import (
"math"
"b612.me/astro/basic"
)
const (
eclipseSeasonNodeDistanceLimitDeg = 35.0
eclipseSeasonMaxSearchStep = 4
)
func nextEclipseSearchCandidateTT(candidateTT float64, phaseType, direction int, synodicMonthDays float64) float64 {
step := eclipseSearchStep(candidateTT, direction, synodicMonthDays)
return basic.CalcMoonSHByJDE(candidateTT+float64(direction*step)*synodicMonthDays, phaseType)
}
func eclipseSearchStep(candidateTT float64, direction int, synodicMonthDays float64) int {
step := 1
for nextStep := 2; nextStep <= eclipseSeasonMaxSearchStep; nextStep++ {
skippedTT := candidateTT + float64(direction*(nextStep-1))*synodicMonthDays
if eclipseNodeDistance(skippedTT) < eclipseSeasonNodeDistanceLimitDeg {
break
}
step = nextStep
}
return step
}
func eclipseNodeDistance(ttJDE float64) float64 {
argument := normalizeDegree360(basic.MoonLonX(ttJDE))
toAscending := math.Min(argument, 360-argument)
toDescending := math.Abs(argument - 180)
return math.Min(toAscending, toDescending)
}
+73
View File
@@ -0,0 +1,73 @@
package eclipse
import (
"testing"
"time"
"b612.me/astro/basic"
)
func TestEclipseSearchStepDoesNotSkipPotentialCandidates(t *testing.T) {
testCases := []struct {
name string
phaseType int
synodicMonthDays float64
potential func(float64) bool
}{
{
name: "solar",
phaseType: 0,
synodicMonthDays: solarEclipseSynodicMonthDays,
potential: isPotentialSolarEclipse,
},
{
name: "lunar",
phaseType: 1,
synodicMonthDays: lunarEclipseSynodicMonthDays,
potential: isPotentialLunarEclipse,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
candidates := eclipseSearchTestCandidates(1600, 800, tc.phaseType, tc.synodicMonthDays)
for index, candidateTT := range candidates {
for _, direction := range []int{-1, 1} {
step := eclipseSearchStep(candidateTT, direction, tc.synodicMonthDays)
for offset := 1; offset < step; offset++ {
skippedIndex := index + direction*offset
if skippedIndex < 0 || skippedIndex >= len(candidates) {
continue
}
if tc.potential(candidates[skippedIndex]) {
t.Fatalf(
"%s skip crosses potential candidate: index=%d direction=%d step=%d offset=%d jd=%.8f",
tc.name,
index,
direction,
step,
offset,
candidates[skippedIndex],
)
}
}
}
}
})
}
}
func eclipseSearchTestCandidates(startYear, years, phaseType int, synodicMonthDays float64) []float64 {
startTT := basic.Date2JDE(time.Date(startYear, 1, 1, 0, 0, 0, 0, time.UTC))
endTT := basic.Date2JDE(time.Date(startYear+years, 1, 1, 0, 0, 0, 0, time.UTC))
candidateTT := basic.CalcMoonSHByJDE(startTT, phaseType)
candidates := make([]float64, 0, years*13)
for candidateTT < endTT {
if candidateTT >= startTT {
candidates = append(candidates, candidateTT)
}
candidateTT = basic.CalcMoonSHByJDE(candidateTT+synodicMonthDays, phaseType)
}
return candidates
}
+8 -1
View File
@@ -11,6 +11,7 @@ const (
solarEclipseSynodicMonthDays = 29.530588853
solarEclipseSearchLimit = 36
solarEclipseSearchEpsilonDay = 1e-8
solarEclipseLatitudeLimitDeg = 2.0
)
type solarEclipseCalculator func(float64) basic.SolarEclipseResult
@@ -236,16 +237,22 @@ func searchSolarEclipse(
candidateTT := basic.CalcMoonSHByJDE(targetTT, 0)
for i := 0; i < solarEclipseSearchLimit; i++ {
if isPotentialSolarEclipse(candidateTT) {
result := calculator(candidateTT)
if result.Type != basic.SolarEclipseNone && solarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) {
return solarEclipseInfoFromBasic(result, date.Location()), true
}
candidateTT = basic.CalcMoonSHByJDE(candidateTT+float64(direction)*solarEclipseSynodicMonthDays, 0)
}
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, solarEclipseSynodicMonthDays)
}
return SolarEclipseInfo{}, false
}
func isPotentialSolarEclipse(newMoonTT float64) bool {
return math.Abs(basic.HMoonTrueBo(newMoonTT)) <= solarEclipseLatitudeLimitDeg
}
func solarEclipseMatchesDirection(greatestTT, targetTT float64, direction int, includeCurrent bool) bool {
delta := greatestTT - targetTT
if math.Abs(delta) <= solarEclipseSearchEpsilonDay {
+126 -6
View File
@@ -217,6 +217,18 @@ func LastGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height f
return info
}
// LastLocalTotalSolarEclipse 上次站心日全食 / previous local total solar eclipse.
// Previous visible local total solar eclipse, using NASA bulletin Split-K by default.
func LastLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalTotalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
}
// LastLocalAnnularSolarEclipse 上次站心日环食 / previous local annular solar eclipse.
// Previous visible local annular solar eclipse, using NASA bulletin Split-K by default.
func LastLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalAnnularSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
}
// NextLocalSolarEclipse 下次站心日食 / next local solar eclipse.
// Next visible local solar eclipse, using NASA bulletin Split-K by default.
func NextLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
@@ -257,6 +269,18 @@ func NextGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height f
return info
}
// NextLocalTotalSolarEclipse 下次站心日全食 / next local total solar eclipse.
// Next visible local total solar eclipse, using NASA bulletin Split-K by default.
func NextLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalTotalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
}
// NextLocalAnnularSolarEclipse 下次站心日环食 / next local annular solar eclipse.
// Next visible local annular solar eclipse, using NASA bulletin Split-K by default.
func NextLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalAnnularSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
}
// ClosestLocalSolarEclipse 最近一次站心日食 / closest local solar eclipse.
// Closest visible local solar eclipse, using NASA bulletin Split-K by default.
func ClosestLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
@@ -301,6 +325,22 @@ func ClosestGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, heigh
return closestLocalSolarEclipse(date, last, hasLast, next, hasNext)
}
// ClosestLocalTotalSolarEclipse 最近一次站心日全食 / closest local total solar eclipse.
// Closest visible local total solar eclipse, using NASA bulletin Split-K by default.
func ClosestLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
last, hasLast := searchLocalTotalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
next, hasNext := searchLocalTotalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
return closestLocalSolarEclipseResult(date, last, hasLast, next, hasNext)
}
// ClosestLocalAnnularSolarEclipse 最近一次站心日环食 / closest local annular solar eclipse.
// Closest visible local annular solar eclipse, using NASA bulletin Split-K by default.
func ClosestLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
last, hasLast := searchLocalAnnularSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
next, hasNext := searchLocalAnnularSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
return closestLocalSolarEclipseResult(date, last, hasLast, next, hasNext)
}
func closestLocalSolarEclipse(
date time.Time,
last LocalSolarEclipseInfo,
@@ -308,21 +348,32 @@ func closestLocalSolarEclipse(
next LocalSolarEclipseInfo,
hasNext bool,
) LocalSolarEclipseInfo {
info, _ := closestLocalSolarEclipseResult(date, last, hasLast, next, hasNext)
return info
}
func closestLocalSolarEclipseResult(
date time.Time,
last LocalSolarEclipseInfo,
hasLast bool,
next LocalSolarEclipseInfo,
hasNext bool,
) (LocalSolarEclipseInfo, bool) {
switch {
case hasLast && !hasNext:
return last
return last, true
case !hasLast && hasNext:
return next
return next, true
case !hasLast && !hasNext:
return LocalSolarEclipseInfo{}
return LocalSolarEclipseInfo{}, false
}
lastDistance := math.Abs(date.Sub(last.GreatestEclipse).Seconds())
nextDistance := math.Abs(next.GreatestEclipse.Sub(date).Seconds())
if lastDistance <= nextDistance {
return last
return last, true
}
return next
return next, true
}
func searchLocalSolarEclipse(
@@ -350,7 +401,69 @@ func searchLocalSolarEclipse(
}
}
}
candidateTT = basic.CalcMoonSHByJDE(candidateTT+float64(direction)*localSolarEclipseSynodicMonthDays, 0)
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, localSolarEclipseSynodicMonthDays)
}
return LocalSolarEclipseInfo{}, false
}
func searchLocalTotalSolarEclipse(
date time.Time,
lon, lat, height float64,
direction int,
includeCurrent bool,
calculator localSolarEclipseCalculator,
mode localSolarEclipseQueryMode,
) (LocalSolarEclipseInfo, bool) {
targetTT := solarEclipseTimeToTTJDE(date)
candidateTT := basic.CalcMoonSHByJDE(targetTT, 0)
for i := 0; i < localSolarEclipseSearchLimit; i++ {
if isPotentialLocalSolarEclipse(candidateTT) {
globalResult := calculator.global(candidateTT)
if globalResult.HasTotal || globalResult.HasHybrid {
result := calculator.local(globalResult.GreatestEclipse, lon, lat, height)
if result.HasTotal {
info := localSolarEclipseInfoFromBasic(result, lon, lat, height, date.Location())
if (mode != localSolarEclipseQueryVisible || localCentralSolarEclipseVisible(info)) &&
localSolarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) {
return info, true
}
}
}
}
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, localSolarEclipseSynodicMonthDays)
}
return LocalSolarEclipseInfo{}, false
}
func searchLocalAnnularSolarEclipse(
date time.Time,
lon, lat, height float64,
direction int,
includeCurrent bool,
calculator localSolarEclipseCalculator,
mode localSolarEclipseQueryMode,
) (LocalSolarEclipseInfo, bool) {
targetTT := solarEclipseTimeToTTJDE(date)
candidateTT := basic.CalcMoonSHByJDE(targetTT, 0)
for i := 0; i < localSolarEclipseSearchLimit; i++ {
if isPotentialLocalSolarEclipse(candidateTT) {
globalResult := calculator.global(candidateTT)
if globalResult.HasAnnular || globalResult.HasHybrid {
result := calculator.local(globalResult.GreatestEclipse, lon, lat, height)
if result.HasAnnular && !result.HasTotal {
info := localSolarEclipseInfoFromBasic(result, lon, lat, height, date.Location())
if (mode != localSolarEclipseQueryVisible || localCentralSolarEclipseVisible(info)) &&
localSolarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) {
return info, true
}
}
}
}
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, localSolarEclipseSynodicMonthDays)
}
return LocalSolarEclipseInfo{}, false
@@ -501,6 +614,13 @@ func localSolarEclipseVisible(info LocalSolarEclipseInfo) bool {
return localSolarEclipseVisibleDuring(info, eventStart, eventEnd)
}
func localCentralSolarEclipseVisible(info LocalSolarEclipseInfo) bool {
if !info.HasCentral || info.CentralStart.IsZero() || info.CentralEnd.IsZero() {
return false
}
return localSolarEclipseVisibleDuring(info, info.CentralStart, info.CentralEnd)
}
func localSolarEclipseVisibleOnDate(info LocalSolarEclipseInfo, dayStart, dayEnd time.Time) bool {
eventStart, eventEnd, ok := localSolarEclipseRange(info)
if !ok {
+121
View File
@@ -129,6 +129,116 @@ func TestLocalSolarEclipseSearchSkipsInvisibleCurrentCandidate(t *testing.T) {
}
}
func TestLocalTotalSolarEclipseSearch(t *testing.T) {
loc := time.UTC
lon, lat, height := -104.1, 25.3, 0.0
date := time.Date(2024, 4, 7, 0, 0, 0, 0, loc)
next, ok := NextLocalTotalSolarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find next local total solar eclipse")
}
if next.Type != SolarEclipseTotal || !next.HasTotal {
t.Fatalf("unexpected next total eclipse: %+v", next)
}
assertSolarTimeClose(t, "NextLocalTotalSolarEclipse", next.GreatestEclipse, time.Date(2024, 4, 8, 18, 17, 15, 0, loc), time.Minute)
assertSolarDurationClose(t, "NextLocalTotalSolarEclipse duration", next.CentralEnd.Sub(next.CentralStart), 4*time.Minute+28*time.Second, 5*time.Second)
last, ok := LastLocalTotalSolarEclipse(next.GreatestEclipse, lon, lat, height)
if !ok {
t.Fatal("expected to find previous local total solar eclipse")
}
if last.Type != SolarEclipseTotal || !last.HasTotal {
t.Fatalf("unexpected last total eclipse: %+v", last)
}
assertSolarTimeClose(t, "LastLocalTotalSolarEclipse", last.GreatestEclipse, next.GreatestEclipse, time.Second)
assertSolarDurationClose(t, "LastLocalTotalSolarEclipse duration", last.CentralEnd.Sub(last.CentralStart), 4*time.Minute+28*time.Second, 5*time.Second)
}
func TestLocalTotalSolarEclipseClosest(t *testing.T) {
loc := time.UTC
lon, lat, height := -104.1, 25.3, 0.0
date := time.Date(2024, 4, 8, 12, 0, 0, 0, loc)
info, ok := ClosestLocalTotalSolarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find closest local total solar eclipse")
}
if info.Type != SolarEclipseTotal || !info.HasTotal {
t.Fatalf("unexpected closest total eclipse: %+v", info)
}
assertSolarTimeClose(t, "ClosestLocalTotalSolarEclipse", info.GreatestEclipse, time.Date(2024, 4, 8, 18, 17, 15, 0, loc), time.Minute)
}
func TestLocalAnnularSolarEclipseSearch(t *testing.T) {
loc := time.UTC
lon, lat, height := -114.5, -22.0, 0.0
date := time.Date(2024, 10, 1, 0, 0, 0, 0, loc)
next, ok := NextLocalAnnularSolarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find next local annular solar eclipse")
}
if next.Type != SolarEclipseAnnular || !next.HasAnnular || next.HasTotal {
t.Fatalf("unexpected next annular eclipse: %+v", next)
}
assertSolarTimeClose(t, "NextLocalAnnularSolarEclipse", next.GreatestEclipse, time.Date(2024, 10, 2, 18, 44, 59, 0, loc), time.Minute)
assertSolarDurationClose(t, "NextLocalAnnularSolarEclipse duration", next.CentralEnd.Sub(next.CentralStart), 7*time.Minute+25*time.Second, 5*time.Second)
last, ok := LastLocalAnnularSolarEclipse(next.GreatestEclipse, lon, lat, height)
if !ok {
t.Fatal("expected to find previous local annular solar eclipse")
}
if last.Type != SolarEclipseAnnular || !last.HasAnnular || last.HasTotal {
t.Fatalf("unexpected last annular eclipse: %+v", last)
}
assertSolarTimeClose(t, "LastLocalAnnularSolarEclipse", last.GreatestEclipse, next.GreatestEclipse, time.Second)
}
func TestLocalAnnularSolarEclipseClosest(t *testing.T) {
loc := time.UTC
lon, lat, height := -114.5, -22.0, 0.0
date := time.Date(2024, 10, 2, 12, 0, 0, 0, loc)
info, ok := ClosestLocalAnnularSolarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find closest local annular solar eclipse")
}
if info.Type != SolarEclipseAnnular || !info.HasAnnular || info.HasTotal {
t.Fatalf("unexpected closest annular eclipse: %+v", info)
}
assertSolarTimeClose(t, "ClosestLocalAnnularSolarEclipse", info.GreatestEclipse, time.Date(2024, 10, 2, 18, 44, 59, 0, loc), time.Minute)
}
func TestLocalCentralSolarEclipseVisibleRequiresCentralPhaseVisibility(t *testing.T) {
info := LocalSolarEclipseInfo{
Type: SolarEclipseTotal,
Longitude: 0,
Latitude: 0,
PartialStart: time.Date(2024, 1, 1, 9, 0, 0, 0, time.UTC),
PartialEnd: time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC),
CentralStart: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
CentralEnd: time.Date(2024, 1, 1, 1, 0, 0, 0, time.UTC),
HasPartial: true,
HasCentral: true,
HasTotal: true,
VisibleAtGreatest: false,
}
if !localSolarEclipseVisible(info) {
t.Fatalf("expected partial phase to be visible")
}
if localCentralSolarEclipseVisible(info) {
t.Fatalf("expected central phase below horizon to be rejected")
}
info.Type = SolarEclipseAnnular
info.HasTotal = false
info.HasAnnular = true
if localCentralSolarEclipseVisible(info) {
t.Fatalf("expected annular central phase below horizon to be rejected")
}
}
func TestLocalSolarEclipseInfoKeepsLocation(t *testing.T) {
loc := time.FixedZone("UTC+08", 8*3600)
lon, lat, height := -104.1, 25.3, 1234.0
@@ -354,3 +464,14 @@ func assertSameLocalSolarEclipse(t *testing.T, name string, got, want LocalSolar
}
assertSolarTimeClose(t, name+".GreatestEclipse", got.GreatestEclipse, want.GreatestEclipse, tolerance)
}
func assertSolarDurationClose(t *testing.T, name string, got, want, tolerance time.Duration) {
t.Helper()
diff := got - want
if diff < 0 {
diff = -diff
}
if diff > tolerance {
t.Fatalf("%s mismatch: got %v want %v diff=%v", name, got, want, diff)
}
}
+151
View File
@@ -0,0 +1,151 @@
package astro_test
import (
"math"
"testing"
"time"
"b612.me/astro/basic"
"b612.me/astro/jupiter"
"b612.me/astro/mars"
"b612.me/astro/mercury"
"b612.me/astro/moon"
"b612.me/astro/neptune"
"b612.me/astro/saturn"
"b612.me/astro/uranus"
"b612.me/astro/venus"
)
func TestPublicPlanetEventBoundaryIncludesCurrent(t *testing.T) {
type eventFuncs struct {
last func(time.Time) time.Time
next func(time.Time) time.Time
}
cases := []struct {
name string
eventUT float64
funcs eventFuncs
}{
{name: "MercuryConjunction", eventUT: basic.NextMercuryConjunction(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastConjunction, next: mercury.NextConjunction}},
{name: "MercuryInferior", eventUT: basic.NextMercuryInferiorConjunctionInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastInferiorConjunction, next: mercury.NextInferiorConjunction}},
{name: "MercurySuperior", eventUT: basic.NextMercurySuperiorConjunctionInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastSuperiorConjunction, next: mercury.NextSuperiorConjunction}},
{name: "MercuryRetrograde", eventUT: basic.NextMercuryRetrogradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastRetrograde, next: mercury.NextRetrograde}},
{name: "MercuryP2R", eventUT: basic.NextMercuryProgradeToRetrogradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastProgradeToRetrograde, next: mercury.NextProgradeToRetrograde}},
{name: "MercuryR2P", eventUT: basic.NextMercuryRetrogradeToProgradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastRetrogradeToPrograde, next: mercury.NextRetrogradeToPrograde}},
{name: "MercuryGreatestElongation", eventUT: basic.NextMercuryGreatestElongationInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastGreatestElongation, next: mercury.NextGreatestElongation}},
{name: "MercuryGreatestElongationEast", eventUT: basic.NextMercuryGreatestElongationEastInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastGreatestElongationEast, next: mercury.NextGreatestElongationEast}},
{name: "MercuryGreatestElongationWest", eventUT: basic.NextMercuryGreatestElongationWestInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastGreatestElongationWest, next: mercury.NextGreatestElongationWest}},
{name: "VenusConjunction", eventUT: basic.NextVenusConjunction(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastConjunction, next: venus.NextConjunction}},
{name: "VenusInferior", eventUT: basic.NextVenusInferiorConjunctionInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastInferiorConjunction, next: venus.NextInferiorConjunction}},
{name: "VenusSuperior", eventUT: basic.NextVenusSuperiorConjunctionInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastSuperiorConjunction, next: venus.NextSuperiorConjunction}},
{name: "VenusRetrograde", eventUT: basic.NextVenusRetrogradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastRetrograde, next: venus.NextRetrograde}},
{name: "VenusP2R", eventUT: basic.NextVenusProgradeToRetrogradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastProgradeToRetrograde, next: venus.NextProgradeToRetrograde}},
{name: "VenusR2P", eventUT: basic.NextVenusRetrogradeToProgradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastRetrogradeToPrograde, next: venus.NextRetrogradeToPrograde}},
{name: "VenusGreatestElongation", eventUT: basic.NextVenusGreatestElongationInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastGreatestElongation, next: venus.NextGreatestElongation}},
{name: "VenusGreatestElongationEast", eventUT: basic.NextVenusGreatestElongationEastInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastGreatestElongationEast, next: venus.NextGreatestElongationEast}},
{name: "VenusGreatestElongationWest", eventUT: basic.NextVenusGreatestElongationWestInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastGreatestElongationWest, next: venus.NextGreatestElongationWest}},
{name: "MarsConjunction", eventUT: basic.NextMarsConjunction(eventBoundaryTT(2026)), funcs: eventFuncs{last: mars.LastConjunction, next: mars.NextConjunction}},
{name: "MarsOpposition", eventUT: basic.NextMarsOpposition(eventBoundaryTT(2026)), funcs: eventFuncs{last: mars.LastOpposition, next: mars.NextOpposition}},
{name: "MarsP2R", eventUT: basic.NextMarsProgradeToRetrograde(eventBoundaryTT(2026)), funcs: eventFuncs{last: mars.LastProgradeToRetrograde, next: mars.NextProgradeToRetrograde}},
{name: "MarsR2P", eventUT: basic.NextMarsRetrogradeToPrograde(eventBoundaryTT(2025)), funcs: eventFuncs{last: mars.LastRetrogradeToPrograde, next: mars.NextRetrogradeToPrograde}},
{name: "MarsEasternQuadrature", eventUT: basic.NextMarsEasternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: mars.LastEasternQuadrature, next: mars.NextEasternQuadrature}},
{name: "MarsWesternQuadrature", eventUT: basic.NextMarsWesternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: mars.LastWesternQuadrature, next: mars.NextWesternQuadrature}},
{name: "JupiterConjunction", eventUT: basic.NextJupiterConjunction(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastConjunction, next: jupiter.NextConjunction}},
{name: "JupiterOpposition", eventUT: basic.NextJupiterOpposition(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastOpposition, next: jupiter.NextOpposition}},
{name: "JupiterP2R", eventUT: basic.NextJupiterProgradeToRetrograde(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastProgradeToRetrograde, next: jupiter.NextProgradeToRetrograde}},
{name: "JupiterR2P", eventUT: basic.NextJupiterRetrogradeToPrograde(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastRetrogradeToPrograde, next: jupiter.NextRetrogradeToPrograde}},
{name: "JupiterEasternQuadrature", eventUT: basic.NextJupiterEasternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastEasternQuadrature, next: jupiter.NextEasternQuadrature}},
{name: "JupiterWesternQuadrature", eventUT: basic.NextJupiterWesternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastWesternQuadrature, next: jupiter.NextWesternQuadrature}},
{name: "SaturnOpposition", eventUT: basic.NextSaturnOpposition(eventBoundaryTT(2025)), funcs: eventFuncs{last: saturn.LastOpposition, next: saturn.NextOpposition}},
{name: "SaturnP2R", eventUT: basic.NextSaturnProgradeToRetrograde(eventBoundaryTT(2025)), funcs: eventFuncs{last: saturn.LastProgradeToRetrograde, next: saturn.NextProgradeToRetrograde}},
{name: "SaturnR2P", eventUT: basic.NextSaturnRetrogradeToPrograde(eventBoundaryTT(2025)), funcs: eventFuncs{last: saturn.LastRetrogradeToPrograde, next: saturn.NextRetrogradeToPrograde}},
{name: "SaturnEasternQuadrature", eventUT: basic.NextSaturnEasternQuadrature(eventBoundaryTT(2025)), funcs: eventFuncs{last: saturn.LastEasternQuadrature, next: saturn.NextEasternQuadrature}},
{name: "SaturnWesternQuadrature", eventUT: basic.NextSaturnWesternQuadrature(eventBoundaryTT(2025)), funcs: eventFuncs{last: saturn.LastWesternQuadrature, next: saturn.NextWesternQuadrature}},
{name: "UranusOpposition", eventUT: basic.NextUranusOpposition(eventBoundaryTT(2025)), funcs: eventFuncs{last: uranus.LastOpposition, next: uranus.NextOpposition}},
{name: "UranusP2R", eventUT: basic.NextUranusProgradeToRetrograde(eventBoundaryTT(2025)), funcs: eventFuncs{last: uranus.LastProgradeToRetrograde, next: uranus.NextProgradeToRetrograde}},
{name: "UranusR2P", eventUT: basic.NextUranusRetrogradeToPrograde(eventBoundaryTT(2025)), funcs: eventFuncs{last: uranus.LastRetrogradeToPrograde, next: uranus.NextRetrogradeToPrograde}},
{name: "UranusEasternQuadrature", eventUT: basic.NextUranusEasternQuadrature(eventBoundaryTT(2025)), funcs: eventFuncs{last: uranus.LastEasternQuadrature, next: uranus.NextEasternQuadrature}},
{name: "UranusWesternQuadrature", eventUT: basic.NextUranusWesternQuadrature(eventBoundaryTT(2025)), funcs: eventFuncs{last: uranus.LastWesternQuadrature, next: uranus.NextWesternQuadrature}},
{name: "NeptuneOpposition", eventUT: basic.NextNeptuneOpposition(eventBoundaryTT(2026)), funcs: eventFuncs{last: neptune.LastOpposition, next: neptune.NextOpposition}},
{name: "NeptuneP2R", eventUT: basic.NextNeptuneProgradeToRetrograde(eventBoundaryTT(2026)), funcs: eventFuncs{last: neptune.LastProgradeToRetrograde, next: neptune.NextProgradeToRetrograde}},
{name: "NeptuneR2P", eventUT: basic.NextNeptuneRetrogradeToPrograde(eventBoundaryTT(2026)), funcs: eventFuncs{last: neptune.LastRetrogradeToPrograde, next: neptune.NextRetrogradeToPrograde}},
{name: "NeptuneEasternQuadrature", eventUT: basic.NextNeptuneEasternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: neptune.LastEasternQuadrature, next: neptune.NextEasternQuadrature}},
{name: "NeptuneWesternQuadrature", eventUT: basic.NextNeptuneWesternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: neptune.LastWesternQuadrature, next: neptune.NextWesternQuadrature}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
eventTime := basic.JDE2DateByZone(tc.eventUT, time.UTC, false)
assertSameEventTime(t, "last", tc.funcs.last(eventTime), eventTime)
assertSameEventTime(t, "next", tc.funcs.next(eventTime), eventTime)
})
}
}
func TestPublicMoonPlanetConjunctionBoundaryIncludesCurrent(t *testing.T) {
type eventFuncs struct {
last func(time.Time) time.Time
next func(time.Time) time.Time
}
cases := []struct {
name string
eventUT float64
funcs eventFuncs
}{
{name: "MoonMercuryConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionMercury), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionMercury) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionMercury) },
}},
{name: "MoonVenusConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionVenus), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionVenus) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionVenus) },
}},
{name: "MoonMarsConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionMars), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionMars) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionMars) },
}},
{name: "MoonJupiterConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionJupiter), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionJupiter) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionJupiter) },
}},
{name: "MoonSaturnConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionSaturn), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionSaturn) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionSaturn) },
}},
{name: "MoonUranusConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionUranus), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionUranus) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionUranus) },
}},
{name: "MoonNeptuneConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionNeptune), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionNeptune) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionNeptune) },
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
eventTime := basic.JDE2DateByZone(tc.eventUT, time.UTC, false)
assertSameEventTime(t, "last", tc.funcs.last(eventTime), eventTime)
assertSameEventTime(t, "next", tc.funcs.next(eventTime), eventTime)
})
}
}
func eventBoundaryTT(year int) float64 {
return basic.TD2UT(basic.Date2JDE(time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)), true)
}
func assertSameEventTime(t *testing.T, name string, got, want time.Time) {
t.Helper()
if math.Abs(got.Sub(want).Seconds()) > 1.0 {
t.Fatalf("%s boundary mismatch: got %s want %s delta %.3fs", name, got, want, got.Sub(want).Seconds())
}
}
+6
View File
@@ -18,6 +18,8 @@ const (
// 返回:
//
// 峰值波长,单位米
//
// Returns the wavelength of maximum emission in meters for a blackbody at the supplied temperature.
func WienPeakWavelength(temperatureK float64) float64 {
if temperatureK <= 0 || math.IsNaN(temperatureK) || math.IsInf(temperatureK, 0) {
return math.NaN()
@@ -32,6 +34,8 @@ func WienPeakWavelength(temperatureK float64) float64 {
// 返回:
//
// 单位面积总出射度,单位 W/m^2
//
// Returns the total radiant exitance in W/m^2 for a blackbody at the supplied temperature.
func StefanBoltzmannFlux(temperatureK float64) float64 {
if temperatureK < 0 || math.IsNaN(temperatureK) || math.IsInf(temperatureK, 0) {
return math.NaN()
@@ -48,6 +52,8 @@ func StefanBoltzmannFlux(temperatureK float64) float64 {
//
// 谱辐亮度,单位 W·sr^-1·m^-3
//
// Returns spectral radiance in W·sr^-1·m^-3 at the supplied wavelength and temperature.
//
// 例:
//
// b := formula.PlanckRadianceByWavelength(500e-9, 5772)
+2
View File
@@ -11,6 +11,8 @@ import "math"
//
// 会合周期,单位与输入相同
//
// Returns the synodic period in the same unit as the two input periods.
//
// 例:
//
// // 地球与金星的会合周期,单位天
+6
View File
@@ -9,6 +9,8 @@ import "math"
// 返回:
//
// 距离模数 m-M
//
// Returns the distance modulus m-M for the supplied distance in parsecs.
func DistanceModulus(distanceParsec float64) float64 {
if distanceParsec <= 0 || math.IsNaN(distanceParsec) || math.IsInf(distanceParsec, 0) {
return math.NaN()
@@ -24,6 +26,8 @@ func DistanceModulus(distanceParsec float64) float64 {
// 返回:
//
// 视星等 m
//
// Returns apparent magnitude from absolute magnitude and distance.
func ApparentMagnitudeFromAbsolute(absoluteMagnitude, distanceParsec float64) float64 {
modulus := DistanceModulus(distanceParsec)
if math.IsNaN(modulus) {
@@ -40,6 +44,8 @@ func ApparentMagnitudeFromAbsolute(absoluteMagnitude, distanceParsec float64) fl
// 返回:
//
// 绝对星等 M
//
// Returns absolute magnitude from apparent magnitude and distance.
func AbsoluteMagnitudeFromApparent(apparentMagnitude, distanceParsec float64) float64 {
modulus := DistanceModulus(distanceParsec)
if math.IsNaN(modulus) {
+14
View File
@@ -16,6 +16,8 @@ const (
// 返回:
//
// 总光度,单位瓦特
//
// Returns stellar luminosity in watts from radius and effective temperature.
func LuminosityFromRadiusTemperature(radiusM, temperatureK float64) float64 {
if radiusM <= 0 || temperatureK <= 0 ||
math.IsNaN(radiusM) || math.IsInf(radiusM, 0) ||
@@ -33,6 +35,8 @@ func LuminosityFromRadiusTemperature(radiusM, temperatureK float64) float64 {
// 返回:
//
// 恒星半径,单位米
//
// Returns stellar radius in meters from luminosity and effective temperature.
func RadiusFromLuminosityTemperature(luminosityW, temperatureK float64) float64 {
if luminosityW <= 0 || temperatureK <= 0 ||
math.IsNaN(luminosityW) || math.IsInf(luminosityW, 0) ||
@@ -54,6 +58,8 @@ func RadiusFromLuminosityTemperature(luminosityW, temperatureK float64) float64
// 返回:
//
// 恒星有效温度,单位开尔文
//
// Returns stellar effective temperature in kelvin from luminosity and radius.
func EffectiveTemperatureFromLuminosityRadius(luminosityW, radiusM float64) float64 {
if luminosityW <= 0 || radiusM <= 0 ||
math.IsNaN(luminosityW) || math.IsInf(luminosityW, 0) ||
@@ -75,6 +81,8 @@ func EffectiveTemperatureFromLuminosityRadius(luminosityW, radiusM float64) floa
// 返回:
//
// 总光度,单位为太阳光度 L☉
//
// Returns luminosity in solar units from radius in solar radii and effective temperature.
func LuminositySolarFromRadiusTemperature(radiusSolar, temperatureK float64) float64 {
if radiusSolar <= 0 || temperatureK <= 0 ||
math.IsNaN(radiusSolar) || math.IsInf(radiusSolar, 0) ||
@@ -92,6 +100,8 @@ func LuminositySolarFromRadiusTemperature(radiusSolar, temperatureK float64) flo
// 返回:
//
// 恒星半径,单位为太阳半径 R☉
//
// Returns radius in solar radii from luminosity in solar units and effective temperature.
func RadiusSolarFromLuminosityTemperature(luminositySolar, temperatureK float64) float64 {
if luminositySolar <= 0 || temperatureK <= 0 ||
math.IsNaN(luminositySolar) || math.IsInf(luminositySolar, 0) ||
@@ -110,6 +120,8 @@ func RadiusSolarFromLuminosityTemperature(luminositySolar, temperatureK float64)
//
// 恒星有效温度,单位开尔文
//
// Returns stellar effective temperature in kelvin from luminosity and radius expressed in solar units.
//
// 例:
//
// // 半径 2.5 R☉、光度 20 L☉ 的主序星
@@ -128,6 +140,8 @@ func EffectiveTemperatureFromLuminositySolarRadius(luminositySolar, radiusSolar
// 返回:
//
// 太阳有效温度,单位开尔文
//
// Returns the adopted solar effective temperature constant in kelvin.
func SolarEffectiveTemperature() float64 {
return solarEffectiveTempK
}
+8
View File
@@ -12,6 +12,8 @@ const darkAdaptedPupilDiameterMM = 7.0
// 返回:
//
// 集光力比值,等于 (diameter1MM / diameter2MM)^2
//
// Returns the light-gathering power ratio, equal to (diameter1MM / diameter2MM)^2.
func LightGatheringPowerRatio(diameter1MM, diameter2MM float64) float64 {
if diameter1MM <= 0 || diameter2MM <= 0 ||
math.IsNaN(diameter1MM) || math.IsInf(diameter1MM, 0) ||
@@ -28,6 +30,8 @@ func LightGatheringPowerRatio(diameter1MM, diameter2MM float64) float64 {
// 返回:
//
// Dawes 极限,单位角秒
//
// Returns the Dawes limit in arcseconds for the supplied aperture.
func DawesLimitArcsec(diameterMM float64) float64 {
if diameterMM <= 0 || math.IsNaN(diameterMM) || math.IsInf(diameterMM, 0) {
return math.NaN()
@@ -42,6 +46,8 @@ func DawesLimitArcsec(diameterMM float64) float64 {
// 返回:
//
// Rayleigh 极限,单位角秒
//
// Returns the Rayleigh limit in arcseconds for the supplied aperture.
func RayleighLimitArcsec(diameterMM float64) float64 {
if diameterMM <= 0 || math.IsNaN(diameterMM) || math.IsInf(diameterMM, 0) {
return math.NaN()
@@ -58,6 +64,8 @@ func RayleighLimitArcsec(diameterMM float64) float64 {
//
// 经验极限星等;这是经验值,不包含天空背景、倍率、透过率和观测经验修正
//
// Returns an empirical limiting magnitude estimate. It does not account for sky background, magnification, transmission, or observer skill.
//
// 例:
//
// // 70mm 小型折射镜,裸眼极限 6 等
+24 -24
View File
@@ -207,8 +207,8 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
// LastConjunction 上一次合日 / previous conjunction with the Sun.
//
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or before date, keeping date's time zone.
func LastConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterConjunction(jde), date.Location(), false)
@@ -216,8 +216,8 @@ func LastConjunction(date time.Time) time.Time {
// NextConjunction 下一次合日 / next conjunction with the Sun.
//
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or after date, keeping date's time zone.
func NextConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterConjunction(jde), date.Location(), false)
@@ -225,8 +225,8 @@ func NextConjunction(date time.Time) time.Time {
// LastOpposition 上一次冲日 / previous opposition.
//
// 返回 date 之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the most recent opposition relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or before date, keeping date's time zone.
func LastOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterOpposition(jde), date.Location(), false)
@@ -234,8 +234,8 @@ func LastOpposition(date time.Time) time.Time {
// NextOpposition 下一次冲日 / next opposition.
//
// 返回 date 之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the next opposition relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or after date, keeping date's time zone.
func NextOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterOpposition(jde), date.Location(), false)
@@ -243,8 +243,8 @@ func NextOpposition(date time.Time) time.Time {
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
//
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from prograde to retrograde, keeping date's time zone.
func LastProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterProgradeToRetrograde(jde), date.Location(), false)
@@ -252,8 +252,8 @@ func LastProgradeToRetrograde(date time.Time) time.Time {
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
//
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from prograde to retrograde, keeping date's time zone.
func NextProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterProgradeToRetrograde(jde), date.Location(), false)
@@ -261,8 +261,8 @@ func NextProgradeToRetrograde(date time.Time) time.Time {
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
//
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from retrograde to prograde, keeping date's time zone.
func LastRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterRetrogradeToPrograde(jde), date.Location(), false)
@@ -270,8 +270,8 @@ func LastRetrogradeToPrograde(date time.Time) time.Time {
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
//
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from retrograde to prograde, keeping date's time zone.
func NextRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterRetrogradeToPrograde(jde), date.Location(), false)
@@ -279,8 +279,8 @@ func NextRetrogradeToPrograde(date time.Time) time.Time {
// LastEasternQuadrature 上一次东方照 / previous eastern quadrature.
//
// 返回 date 之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the most recent eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or before date, keeping date's time zone.
func LastEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterEasternQuadrature(jde), date.Location(), false)
@@ -288,8 +288,8 @@ func LastEasternQuadrature(date time.Time) time.Time {
// NextEasternQuadrature 下一次东方照 / next eastern quadrature.
//
// 返回 date 之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the next eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or after date, keeping date's time zone.
func NextEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterEasternQuadrature(jde), date.Location(), false)
@@ -297,8 +297,8 @@ func NextEasternQuadrature(date time.Time) time.Time {
// LastWesternQuadrature 上一次西方照 / previous western quadrature.
//
// 返回 date 之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the most recent western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or before date, keeping date's time zone.
func LastWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterWesternQuadrature(jde), date.Location(), false)
@@ -306,8 +306,8 @@ func LastWesternQuadrature(date time.Time) time.Time {
// NextWesternQuadrature 下一次西方照 / next western quadrature.
//
// 返回 date 之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the next western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or after date, keeping date's time zone.
func NextWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterWesternQuadrature(jde), date.Location(), false)
+24 -24
View File
@@ -207,8 +207,8 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
// LastConjunction 上一次合日 / previous conjunction with the Sun.
//
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or before date, keeping date's time zone.
func LastConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsConjunction(jde), date.Location(), false)
@@ -216,8 +216,8 @@ func LastConjunction(date time.Time) time.Time {
// NextConjunction 下一次合日 / next conjunction with the Sun.
//
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or after date, keeping date's time zone.
func NextConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsConjunction(jde), date.Location(), false)
@@ -225,8 +225,8 @@ func NextConjunction(date time.Time) time.Time {
// LastOpposition 上一次冲日 / previous opposition.
//
// 返回 date 之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the most recent opposition relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or before date, keeping date's time zone.
func LastOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsOpposition(jde), date.Location(), false)
@@ -234,8 +234,8 @@ func LastOpposition(date time.Time) time.Time {
// NextOpposition 下一次冲日 / next opposition.
//
// 返回 date 之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the next opposition relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or after date, keeping date's time zone.
func NextOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsOpposition(jde), date.Location(), false)
@@ -243,8 +243,8 @@ func NextOpposition(date time.Time) time.Time {
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
//
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from prograde to retrograde, keeping date's time zone.
func LastProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsProgradeToRetrograde(jde), date.Location(), false)
@@ -252,8 +252,8 @@ func LastProgradeToRetrograde(date time.Time) time.Time {
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
//
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from prograde to retrograde, keeping date's time zone.
func NextProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsProgradeToRetrograde(jde), date.Location(), false)
@@ -261,8 +261,8 @@ func NextProgradeToRetrograde(date time.Time) time.Time {
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
//
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from retrograde to prograde, keeping date's time zone.
func LastRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsRetrogradeToPrograde(jde), date.Location(), false)
@@ -270,8 +270,8 @@ func LastRetrogradeToPrograde(date time.Time) time.Time {
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
//
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from retrograde to prograde, keeping date's time zone.
func NextRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsRetrogradeToPrograde(jde), date.Location(), false)
@@ -279,8 +279,8 @@ func NextRetrogradeToPrograde(date time.Time) time.Time {
// LastEasternQuadrature 上一次东方照 / previous eastern quadrature.
//
// 返回 date 之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the most recent eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or before date, keeping date's time zone.
func LastEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsEasternQuadrature(jde), date.Location(), false)
@@ -288,8 +288,8 @@ func LastEasternQuadrature(date time.Time) time.Time {
// NextEasternQuadrature 下一次东方照 / next eastern quadrature.
//
// 返回 date 之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the next eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or after date, keeping date's time zone.
func NextEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsEasternQuadrature(jde), date.Location(), false)
@@ -297,8 +297,8 @@ func NextEasternQuadrature(date time.Time) time.Time {
// LastWesternQuadrature 上一次西方照 / previous western quadrature.
//
// 返回 date 之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the most recent western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or before date, keeping date's time zone.
func LastWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsWesternQuadrature(jde), date.Location(), false)
@@ -306,8 +306,8 @@ func LastWesternQuadrature(date time.Time) time.Time {
// NextWesternQuadrature 下一次西方照 / next western quadrature.
//
// 返回 date 之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the next western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or after date, keeping date's time zone.
func NextWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsWesternQuadrature(jde), date.Location(), false)
+52 -52
View File
@@ -207,8 +207,8 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
// LastConjunction 上一次合日 / previous conjunction with the Sun.
//
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or before date, keeping date's time zone.
func LastConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryConjunction(jde), date.Location(), false)
@@ -216,8 +216,8 @@ func LastConjunction(date time.Time) time.Time {
// NextConjunction 下一次合日 / next conjunction with the Sun.
//
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or after date, keeping date's time zone.
func NextConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryConjunction(jde), date.Location(), false)
@@ -225,144 +225,144 @@ func NextConjunction(date time.Time) time.Time {
// LastInferiorConjunction 上一次下合 / previous inferior conjunction.
//
// 返回 date 之前最近一次下合时刻,结果保持 date 的时区。
// Returns the most recent inferior conjunction relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次下合时刻,结果保持 date 的时区。
// Returns the nearest inferior conjunction at or before date, keeping date's time zone.
func LastInferiorConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryInferiorConjunction(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryInferiorConjunctionInclusive(jde), date.Location(), false)
}
// NextInferiorConjunction 下一次下合 / next inferior conjunction.
//
// 返回 date 之后最近一次下合时刻,结果保持 date 的时区。
// Returns the next inferior conjunction relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次下合时刻,结果保持 date 的时区。
// Returns the nearest inferior conjunction at or after date, keeping date's time zone.
func NextInferiorConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryInferiorConjunction(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryInferiorConjunctionInclusive(jde), date.Location(), false)
}
// LastSuperiorConjunction 上一次上合 / previous superior conjunction.
//
// 返回 date 之前最近一次上合时刻,结果保持 date 的时区。
// Returns the most recent superior conjunction relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次上合时刻,结果保持 date 的时区。
// Returns the nearest superior conjunction at or before date, keeping date's time zone.
func LastSuperiorConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercurySuperiorConjunction(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercurySuperiorConjunctionInclusive(jde), date.Location(), false)
}
// NextSuperiorConjunction 下一次上合 / next superior conjunction.
//
// 返回 date 之后最近一次上合时刻,结果保持 date 的时区。
// Returns the next superior conjunction relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次上合时刻,结果保持 date 的时区。
// Returns the nearest superior conjunction at or after date, keeping date's time zone.
func NextSuperiorConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercurySuperiorConjunction(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercurySuperiorConjunctionInclusive(jde), date.Location(), false)
}
// LastRetrograde 上一次留 / previous stationary point.
//
// 返回 date 之前最近一次留时刻,不区分顺转逆还是逆转顺,结果保持 date 的时区。
// Returns the most recent stationary point relative to date without distinguishing direction, keeping date's time zone.
// 返回 date 当前或之前最近一次留时刻,不区分顺转逆还是逆转顺,结果保持 date 的时区。
// Returns the nearest stationary point at or before date, regardless of the direction change, keeping date's time zone.
func LastRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryRetrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryRetrogradeInclusive(jde), date.Location(), false)
}
// NextRetrograde 下一次留 / next stationary point.
//
// 返回 date 之后最近一次留时刻,不区分顺转逆还是逆转顺,结果保持 date 的时区。
// Returns the next stationary point relative to date without distinguishing direction, keeping date's time zone.
// 返回 date 当前或之后最近一次留时刻,不区分顺转逆还是逆转顺,结果保持 date 的时区。
// Returns the nearest stationary point at or after date, regardless of the direction change, keeping date's time zone.
func NextRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryRetrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryRetrogradeInclusive(jde), date.Location(), false)
}
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
//
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from prograde to retrograde, keeping date's time zone.
func LastProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryProgradeToRetrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryProgradeToRetrogradeInclusive(jde), date.Location(), false)
}
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
//
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from prograde to retrograde, keeping date's time zone.
func NextProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryProgradeToRetrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryProgradeToRetrogradeInclusive(jde), date.Location(), false)
}
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
//
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from retrograde to prograde, keeping date's time zone.
func LastRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryRetrogradeToPrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryRetrogradeToProgradeInclusive(jde), date.Location(), false)
}
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
//
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from retrograde to prograde, keeping date's time zone.
func NextRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryRetrogradeToPrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryRetrogradeToProgradeInclusive(jde), date.Location(), false)
}
// LastGreatestElongation 上一次大距 / previous greatest elongation.
//
// 返回 date 之前最近一次大距时刻,不区分东西大距,结果保持 date 的时区。
// Returns the most recent greatest elongation relative to date without distinguishing east or west, keeping date's time zone.
// 返回 date 当前或之前最近一次大距时刻,不区分东西大距,结果保持 date 的时区。
// Returns the nearest greatest elongation at or before date, regardless of east or west, keeping date's time zone.
func LastGreatestElongation(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongation(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongationInclusive(jde), date.Location(), false)
}
// NextGreatestElongation 下一次大距 / next greatest elongation.
//
// 返回 date 之后最近一次大距时刻,不区分东西大距,结果保持 date 的时区。
// Returns the next greatest elongation relative to date without distinguishing east or west, keeping date's time zone.
// 返回 date 当前或之后最近一次大距时刻,不区分东西大距,结果保持 date 的时区。
// Returns the nearest greatest elongation at or after date, regardless of east or west, keeping date's time zone.
func NextGreatestElongation(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongation(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongationInclusive(jde), date.Location(), false)
}
// LastGreatestElongationEast 上一次东大距 / previous greatest eastern elongation.
//
// 返回 date 之前最近一次东大距时刻,结果保持 date 的时区。
// Returns the most recent greatest eastern elongation relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次东大距时刻,结果保持 date 的时区。
// Returns the nearest eastern greatest elongation at or before date, keeping date's time zone.
func LastGreatestElongationEast(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongationEast(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongationEastInclusive(jde), date.Location(), false)
}
// NextGreatestElongationEast 下一次东大距 / next greatest eastern elongation.
//
// 返回 date 之后最近一次东大距时刻,结果保持 date 的时区。
// Returns the next greatest eastern elongation relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次东大距时刻,结果保持 date 的时区。
// Returns the nearest eastern greatest elongation at or after date, keeping date's time zone.
func NextGreatestElongationEast(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongationEast(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongationEastInclusive(jde), date.Location(), false)
}
// LastGreatestElongationWest 上一次西大距 / previous greatest western elongation.
//
// 返回 date 之前最近一次西大距时刻,结果保持 date 的时区。
// Returns the most recent greatest western elongation relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次西大距时刻,结果保持 date 的时区。
// Returns the nearest western greatest elongation at or before date, keeping date's time zone.
func LastGreatestElongationWest(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongationWest(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongationWestInclusive(jde), date.Location(), false)
}
// NextGreatestElongationWest 下一次西大距 / next greatest western elongation.
//
// 返回 date 之后最近一次西大距时刻,结果保持 date 的时区。
// Returns the next greatest western elongation relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次西大距时刻,结果保持 date 的时区。
// Returns the nearest western greatest elongation at or after date, keeping date's time zone.
func NextGreatestElongationWest(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongationWest(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongationWestInclusive(jde), date.Location(), false)
}
+73
View File
@@ -0,0 +1,73 @@
package mercury
import (
"time"
"b612.me/astro/basic"
)
// TransitInfo 地心水星凌日信息 / geocentric Mercury transit information.
//
// Start、Greatest、End、InternalStart、InternalEnd 都保持调用者输入的时区。
// 内切接触不存在时 InternalStart / InternalEnd 为零值。
// Start, Greatest, End, InternalStart, and InternalEnd preserve the caller's timezone.
// InternalStart and InternalEnd are zero values when internal contacts do not exist.
type TransitInfo struct {
Valid bool
Start time.Time
InternalStart time.Time
Greatest time.Time
InternalEnd time.Time
End time.Time
Duration time.Duration
InternalDuration time.Duration
MinimumSeparationArcsec float64
SunSemidiameterArcsec float64
PlanetSemidiameterArcsec float64
HasInternal bool
}
// NextTransit 下一次地心水星凌日 / next geocentric Mercury transit.
func NextTransit(date time.Time) TransitInfo {
return transitInfoFromBasic(basic.NextMercuryTransit(basic.Date2JDE(date.UTC())), date.Location())
}
// LastTransit 上一次地心水星凌日 / previous geocentric Mercury transit.
func LastTransit(date time.Time) TransitInfo {
return transitInfoFromBasic(basic.LastMercuryTransit(basic.Date2JDE(date.UTC())), date.Location())
}
// ClosestTransit 最近一次地心水星凌日 / closest geocentric Mercury transit.
func ClosestTransit(date time.Time) TransitInfo {
return transitInfoFromBasic(basic.ClosestMercuryTransit(basic.Date2JDE(date.UTC())), date.Location())
}
func transitInfoFromBasic(result basic.PlanetTransitResult, loc *time.Location) TransitInfo {
if !result.Valid {
return TransitInfo{}
}
start := basic.JDE2DateByZone(result.ExternalIngress, loc, false)
greatest := basic.JDE2DateByZone(result.Greatest, loc, false)
end := basic.JDE2DateByZone(result.ExternalEgress, loc, false)
info := TransitInfo{
Valid: true,
Start: start,
Greatest: greatest,
End: end,
Duration: end.Sub(start),
MinimumSeparationArcsec: result.MinimumSeparationArcsec,
SunSemidiameterArcsec: result.SunSemidiameterArcsec,
PlanetSemidiameterArcsec: result.PlanetSemidiameterArcsec,
HasInternal: result.HasInternal,
}
if result.HasInternal {
info.InternalStart = basic.JDE2DateByZone(result.InternalIngress, loc, false)
info.InternalEnd = basic.JDE2DateByZone(result.InternalEgress, loc, false)
info.InternalDuration = info.InternalEnd.Sub(info.InternalStart)
}
return info
}
+23
View File
@@ -0,0 +1,23 @@
package mercury
import (
"testing"
"time"
)
func TestTransitWrappers(t *testing.T) {
loc := time.FixedZone("CST", 8*3600)
info := NextTransit(time.Date(2019, 1, 1, 0, 0, 0, 0, loc))
if !info.Valid {
t.Fatal("expected valid transit")
}
if info.Greatest.Location() != loc {
t.Fatalf("timezone mismatch: got %v want %v", info.Greatest.Location(), loc)
}
if info.Greatest.Year() != 2019 || info.Greatest.Month() != time.November || info.Greatest.Day() != 11 {
t.Fatalf("unexpected greatest time: %s", info.Greatest)
}
if !info.HasInternal || info.Duration <= 0 || info.InternalDuration <= 0 {
t.Fatalf("unexpected durations: %+v", info)
}
}
+72
View File
@@ -0,0 +1,72 @@
package moon
import (
"time"
"b612.me/astro/basic"
)
// ConjunctionPlanet 月球合月目标行星 / target planet for Moon-planet conjunction.
type ConjunctionPlanet string
const (
ConjunctionMercury ConjunctionPlanet = "mercury"
ConjunctionVenus ConjunctionPlanet = "venus"
ConjunctionMars ConjunctionPlanet = "mars"
ConjunctionJupiter ConjunctionPlanet = "jupiter"
ConjunctionSaturn ConjunctionPlanet = "saturn"
ConjunctionUranus ConjunctionPlanet = "uranus"
ConjunctionNeptune ConjunctionPlanet = "neptune"
)
func conjunctionPlanetToBasic(planet ConjunctionPlanet) basic.MoonPlanetConjunctionPlanet {
switch planet {
case ConjunctionMercury:
return basic.MoonPlanetConjunctionMercury
case ConjunctionVenus:
return basic.MoonPlanetConjunctionVenus
case ConjunctionMars:
return basic.MoonPlanetConjunctionMars
case ConjunctionJupiter:
return basic.MoonPlanetConjunctionJupiter
case ConjunctionSaturn:
return basic.MoonPlanetConjunctionSaturn
case ConjunctionUranus:
return basic.MoonPlanetConjunctionUranus
case ConjunctionNeptune:
return basic.MoonPlanetConjunctionNeptune
default:
return 0
}
}
func validConjunctionPlanet(planet ConjunctionPlanet) bool {
return conjunctionPlanetToBasic(planet) != 0
}
// LastConjunctionWithPlanet 上一次行星合月(赤经合) / previous Moon-planet conjunction.
func LastConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
if !validConjunctionPlanet(planet) {
return time.Time{}
}
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false)
}
// NextConjunctionWithPlanet 下一次行星合月(赤经合) / next Moon-planet conjunction.
func NextConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
if !validConjunctionPlanet(planet) {
return time.Time{}
}
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false)
}
// ClosestConjunctionWithPlanet 最近一次行星合月(赤经合) / closest Moon-planet conjunction.
func ClosestConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
if !validConjunctionPlanet(planet) {
return time.Time{}
}
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.ClosestMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false)
}
+103
View File
@@ -0,0 +1,103 @@
package moon
import (
"math"
"testing"
"time"
"b612.me/astro/basic"
)
func TestConjunctionPlanetWrappersMatchBasic(t *testing.T) {
loc := time.FixedZone("CST", 8*3600)
query := time.Date(2026, 1, 15, 20, 0, 0, 0, loc)
queryTT := basic.TD2UT(basic.Date2JDE(query.UTC()), true)
cases := []struct {
name string
planet ConjunctionPlanet
basic basic.MoonPlanetConjunctionPlanet
}{
{name: "Mercury", planet: ConjunctionMercury, basic: basic.MoonPlanetConjunctionMercury},
{name: "Venus", planet: ConjunctionVenus, basic: basic.MoonPlanetConjunctionVenus},
{name: "Mars", planet: ConjunctionMars, basic: basic.MoonPlanetConjunctionMars},
{name: "Jupiter", planet: ConjunctionJupiter, basic: basic.MoonPlanetConjunctionJupiter},
{name: "Saturn", planet: ConjunctionSaturn, basic: basic.MoonPlanetConjunctionSaturn},
{name: "Uranus", planet: ConjunctionUranus, basic: basic.MoonPlanetConjunctionUranus},
{name: "Neptune", planet: ConjunctionNeptune, basic: basic.MoonPlanetConjunctionNeptune},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assertSameConjunctionTime(t, "last", LastConjunctionWithPlanet(query, tc.planet), basic.LastMoonPlanetConjunction(queryTT, tc.basic), loc)
assertSameConjunctionTime(t, "next", NextConjunctionWithPlanet(query, tc.planet), basic.NextMoonPlanetConjunction(queryTT, tc.basic), loc)
assertSameConjunctionTime(t, "closest", ClosestConjunctionWithPlanet(query, tc.planet), basic.ClosestMoonPlanetConjunction(queryTT, tc.basic), loc)
})
}
}
func assertSameConjunctionTime(t *testing.T, name string, got time.Time, wantJDE float64, loc *time.Location) {
t.Helper()
want := basic.JDE2DateByZone(wantJDE, loc, false)
if got.Location() != loc {
t.Fatalf("%s location mismatch: got %q want %q", name, got.Location().String(), loc.String())
}
if !got.Equal(want) {
t.Fatalf("%s time mismatch: got %s want %s", name, got.Format(time.RFC3339Nano), want.Format(time.RFC3339Nano))
}
}
func TestClosestConjunctionReturnsNearestCandidate(t *testing.T) {
query := time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)
last := LastConjunctionWithPlanet(query, ConjunctionMercury)
next := NextConjunctionWithPlanet(query, ConjunctionMercury)
got := ClosestConjunctionWithPlanet(query, ConjunctionMercury)
lastDiff := math.Abs(query.Sub(last).Seconds())
nextDiff := math.Abs(next.Sub(query).Seconds())
if lastDiff <= nextDiff {
if !got.Equal(last) {
t.Fatalf("closest should match last: got %s want %s", got.Format(time.RFC3339Nano), last.Format(time.RFC3339Nano))
}
return
}
if !got.Equal(next) {
t.Fatalf("closest should match next: got %s want %s", got.Format(time.RFC3339Nano), next.Format(time.RFC3339Nano))
}
}
func TestInvalidConjunctionPlanetReturnsZeroTime(t *testing.T) {
query := time.Date(2026, 1, 15, 12, 0, 0, 0, time.FixedZone("CST", 8*3600))
invalid := ConjunctionPlanet("pluto")
for name, fn := range map[string]func(time.Time, ConjunctionPlanet) time.Time{
"last": LastConjunctionWithPlanet,
"next": NextConjunctionWithPlanet,
"closest": ClosestConjunctionWithPlanet,
} {
if got := fn(query, invalid); !got.IsZero() {
t.Fatalf("%s should return zero time for invalid planet, got %s", name, got.Format(time.RFC3339Nano))
}
}
}
func TestNextConjunctionAdvancesPastReturnedEvent(t *testing.T) {
cursor := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
first := NextConjunctionWithPlanet(cursor, ConjunctionMercury)
query := first.Add(time.Second)
next := NextConjunctionWithPlanet(query, ConjunctionMercury)
if !next.After(query) {
t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%v",
query.Format(time.RFC3339Nano),
next.Format(time.RFC3339Nano),
next.Sub(query),
)
}
if next.Equal(first) {
t.Fatalf("expected next conjunction to advance: first=%s next=%s",
first.Format(time.RFC3339Nano),
next.Format(time.RFC3339Nano),
)
}
}
+43
View File
@@ -0,0 +1,43 @@
package moon
import (
"math"
"testing"
"time"
"b612.me/astro/basic"
)
func TestGeocentricApparentRaDecComponentsMatch(t *testing.T) {
date := time.Date(2026, 1, 1, 6, 0, 0, 0, time.UTC)
ra, dec := GeocentricApparentRaDec(date)
if diff := math.Abs(ra - GeocentricApparentRa(date)); diff > 1e-12 {
t.Fatalf("RA pair mismatch: got %.15f want %.15f", ra, GeocentricApparentRa(date))
}
if diff := math.Abs(dec - GeocentricApparentDec(date)); diff > 1e-12 {
t.Fatalf("Dec pair mismatch: got %.15f want %.15f", dec, GeocentricApparentDec(date))
}
}
func TestGeocentricApparentRaDecDiffersFromTopocentricAtSite(t *testing.T) {
date := time.Date(2026, 1, 1, 6, 0, 0, 0, time.FixedZone("CST", 8*3600))
geoRA, geoDec := GeocentricApparentRaDec(date)
topoRA, topoDec := ApparentRaDec(date, 121.4737, 31.2304)
if math.Abs(geoRA-topoRA) < 1e-6 && math.Abs(geoDec-topoDec) < 1e-6 {
t.Fatalf("geocentric apparent RA/Dec unexpectedly matches topocentric values: geo=(%.12f, %.12f) topo=(%.12f, %.12f)",
geoRA, geoDec, topoRA, topoDec)
}
}
func TestTrueRaDecUsesBasicGeocentricTrue(t *testing.T) {
date := time.Date(2026, 1, 1, 6, 0, 0, 0, time.UTC)
wantRA, wantDec := basic.HMoonGeocentricTrueRaDec(basic.TD2UT(basic.Date2JDE(date.UTC()), true))
gotRA, gotDec := TrueRaDec(date)
if math.Abs(gotRA-wantRA) > 1e-12 || math.Abs(gotDec-wantDec) > 1e-12 {
t.Fatalf("TrueRaDec mismatch: got (%.15f, %.15f) want (%.15f, %.15f)", gotRA, gotDec, wantRA, wantDec)
}
}
+30 -3
View File
@@ -84,7 +84,7 @@ func ApparentLo(date time.Time) float64 {
// Returns the Moon's geocentric true right ascension at the instant represented by date, in degrees.
func TrueRa(date time.Time) float64 {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonTrueRa(basic.TD2UT(jde, true))
return basic.HMoonGeocentricTrueRa(basic.TD2UT(jde, true))
}
// TrueDec 月亮地心真赤纬 / true geocentric declination.
@@ -93,7 +93,7 @@ func TrueRa(date time.Time) float64 {
// Returns the Moon's geocentric true declination at the instant represented by date, in degrees.
func TrueDec(date time.Time) float64 {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonTrueDec(basic.TD2UT(jde, true))
return basic.HMoonGeocentricTrueDec(basic.TD2UT(jde, true))
}
// TrueRaDec 月亮地心真赤经、真赤纬 / true geocentric right ascension and declination.
@@ -102,7 +102,34 @@ func TrueDec(date time.Time) float64 {
// Returns the Moon's geocentric true right ascension and declination at the instant represented by date, in degrees.
func TrueRaDec(date time.Time) (float64, float64) {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonTrueRaDec(basic.TD2UT(jde, true))
return basic.HMoonGeocentricTrueRaDec(basic.TD2UT(jde, true))
}
// GeocentricApparentRa 月亮地心视赤经 / apparent geocentric right ascension.
//
// 返回月亮在 date 对应绝对时刻的地心视赤经,单位度。
// Returns the Moon's apparent geocentric right ascension at the instant represented by date, in degrees.
func GeocentricApparentRa(date time.Time) float64 {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonGeocentricApparentRa(basic.TD2UT(jde, true))
}
// GeocentricApparentDec 月亮地心视赤纬 / apparent geocentric declination.
//
// 返回月亮在 date 对应绝对时刻的地心视赤纬,单位度。
// Returns the Moon's apparent geocentric declination at the instant represented by date, in degrees.
func GeocentricApparentDec(date time.Time) float64 {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonGeocentricApparentDec(basic.TD2UT(jde, true))
}
// GeocentricApparentRaDec 月亮地心视赤经、视赤纬 / apparent geocentric right ascension and declination.
//
// 返回月亮在 date 对应绝对时刻的地心视赤经与视赤纬,单位度。
// Returns the Moon's apparent geocentric right ascension and declination at the instant represented by date, in degrees.
func GeocentricApparentRaDec(date time.Time) (float64, float64) {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonGeocentricApparentRaDec(basic.TD2UT(jde, true))
}
// ApparentRa 月亮站心视赤经 / apparent topocentric right ascension.
+24 -24
View File
@@ -207,8 +207,8 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
// LastConjunction 上一次合日 / previous conjunction with the Sun.
//
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or before date, keeping date's time zone.
func LastConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneConjunction(jde), date.Location(), false)
@@ -216,8 +216,8 @@ func LastConjunction(date time.Time) time.Time {
// NextConjunction 下一次合日 / next conjunction with the Sun.
//
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or after date, keeping date's time zone.
func NextConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneConjunction(jde), date.Location(), false)
@@ -225,8 +225,8 @@ func NextConjunction(date time.Time) time.Time {
// LastOpposition 上一次冲日 / previous opposition.
//
// 返回 date 之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the most recent opposition relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or before date, keeping date's time zone.
func LastOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneOpposition(jde), date.Location(), false)
@@ -234,8 +234,8 @@ func LastOpposition(date time.Time) time.Time {
// NextOpposition 下一次冲日 / next opposition.
//
// 返回 date 之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the next opposition relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or after date, keeping date's time zone.
func NextOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneOpposition(jde), date.Location(), false)
@@ -243,8 +243,8 @@ func NextOpposition(date time.Time) time.Time {
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
//
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from prograde to retrograde, keeping date's time zone.
func LastProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneProgradeToRetrograde(jde), date.Location(), false)
@@ -252,8 +252,8 @@ func LastProgradeToRetrograde(date time.Time) time.Time {
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
//
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from prograde to retrograde, keeping date's time zone.
func NextProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneProgradeToRetrograde(jde), date.Location(), false)
@@ -261,8 +261,8 @@ func NextProgradeToRetrograde(date time.Time) time.Time {
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
//
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from retrograde to prograde, keeping date's time zone.
func LastRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneRetrogradeToPrograde(jde), date.Location(), false)
@@ -270,8 +270,8 @@ func LastRetrogradeToPrograde(date time.Time) time.Time {
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
//
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from retrograde to prograde, keeping date's time zone.
func NextRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneRetrogradeToPrograde(jde), date.Location(), false)
@@ -279,8 +279,8 @@ func NextRetrogradeToPrograde(date time.Time) time.Time {
// LastEasternQuadrature 上一次东方照 / previous eastern quadrature.
//
// 返回 date 之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the most recent eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or before date, keeping date's time zone.
func LastEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneEasternQuadrature(jde), date.Location(), false)
@@ -288,8 +288,8 @@ func LastEasternQuadrature(date time.Time) time.Time {
// NextEasternQuadrature 下一次东方照 / next eastern quadrature.
//
// 返回 date 之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the next eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or after date, keeping date's time zone.
func NextEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneEasternQuadrature(jde), date.Location(), false)
@@ -297,8 +297,8 @@ func NextEasternQuadrature(date time.Time) time.Time {
// LastWesternQuadrature 上一次西方照 / previous western quadrature.
//
// 返回 date 之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the most recent western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or before date, keeping date's time zone.
func LastWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneWesternQuadrature(jde), date.Location(), false)
@@ -306,8 +306,8 @@ func LastWesternQuadrature(date time.Time) time.Time {
// NextWesternQuadrature 下一次西方照 / next western quadrature.
//
// 返回 date 之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the next western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or after date, keeping date's time zone.
func NextWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneWesternQuadrature(jde), date.Location(), false)
+6 -1
View File
@@ -1,14 +1,19 @@
package neptune
import (
"math"
"testing"
"time"
)
func sameUnixSecond(got time.Time, want int64) bool {
return math.Abs(float64(got.Unix()-want)) <= 1
}
func TestNeptune(t *testing.T) {
tz := time.FixedZone("CST", 8*3600)
date := time.Date(2022, 01, 20, 00, 00, 00, 00, tz)
if NextConjunction(date).Unix() != 1647171800 {
if !sameUnixSecond(NextConjunction(date), 1647171800) {
t.Fatal(NextConjunction(date).Unix())
}
if CulminationTime(date, 115).Unix() != 1642665021 {
+33 -3
View File
@@ -12,13 +12,17 @@ var (
ERR_ORBIT_NEVER_SET = errors.New("ERROR:轨道目标今日永远在地平线上!")
)
// Elements 日心二体圆锥曲线根数,参考系为 J2000 平黄道/平春分点。
// Elements 日心二体圆锥曲线根数 / heliocentric two-body conic elements.
// 参考系为 J2000 平黄道/平春分点。
// The reference frame is the J2000 mean ecliptic and mean equinox.
// EpochJD 与 TpJD 使用 TT/TDB 对应的儒略日。
// EpochJD and TpJD are Julian days on the TT/TDB scale.
//
// 经典椭圆根数:A/E/I/Omega/W/M0
// 近日点形式:Q/E/I/Omega/W/TpJD
//
// 线性 rates 仅作用于经典椭圆根数,单位均为每天变化量。
// The linear rates apply only to the classical elliptical element form and are expressed per day.
type Elements struct {
EpochJD float64 // 历元儒略日(TT/TDB / epoch Julian day in TT/TDB.
A float64 // 半长径,单位 AU / semi-major axis in AU.
@@ -38,14 +42,20 @@ type Elements struct {
MDot float64 // 平近点角日变化,单位 deg/day / daily rate of M.
}
// EclipticPosition 黄道球坐标结果Lon/Lat 单位度,Distance 单位 AU。
// EclipticPosition 黄道球坐标结果 / ecliptic spherical coordinates.
//
// Lon/Lat 单位度,Distance 单位 AU。
// Lon/Lat are in degrees and Distance is in AU.
type EclipticPosition struct {
Lon float64
Lat float64
Distance float64
}
// EquatorialPosition 赤道球坐标结果RA/Dec 单位度,Distance 单位 AU。
// EquatorialPosition 赤道球坐标结果 / equatorial spherical coordinates.
//
// RA/Dec 单位度,Distance 单位 AU。
// RA/Dec are in degrees and Distance is in AU.
type EquatorialPosition struct {
RA float64
Dec float64
@@ -55,6 +65,7 @@ type EquatorialPosition struct {
// MeanMotion 平均角速度 / mean motion.
//
// 返回平均角速度,单位度/日;对抛物线和双曲线轨道返回 `NaN`。
// Returns mean motion in degrees per day. Parabolic and hyperbolic cases return `NaN`.
func MeanMotion(elements Elements) float64 {
return basic.OrbitMeanMotion(toBasicElements(elements))
}
@@ -62,6 +73,7 @@ func MeanMotion(elements Elements) float64 {
// MeanAnomaly 平近点角 / mean anomaly.
//
// 返回给定时刻的平近点角,单位度;对抛物线和双曲线轨道返回 `NaN`。
// Returns mean anomaly in degrees for the supplied instant. Parabolic and hyperbolic cases return `NaN`.
func MeanAnomaly(date time.Time, elements Elements) float64 {
return basic.OrbitMeanAnomaly(ttJulianDay(date), toBasicElements(elements))
}
@@ -69,6 +81,7 @@ func MeanAnomaly(date time.Time, elements Elements) float64 {
// TrueAnomaly 真近点角 / true anomaly.
//
// 返回给定时刻的真近点角,单位度。
// Returns true anomaly in degrees for the supplied instant.
func TrueAnomaly(date time.Time, elements Elements) float64 {
return basic.OrbitTrueAnomaly(ttJulianDay(date), toBasicElements(elements))
}
@@ -76,6 +89,7 @@ func TrueAnomaly(date time.Time, elements Elements) float64 {
// HeliocentricEclipticJ2000 日心 J2000 平黄道坐标 / heliocentric J2000 ecliptic coordinates.
//
// 返回黄经、黄纬和距离;角度单位度,距离单位 AU。
// Returns heliocentric J2000 ecliptic longitude, latitude, and distance. Angles are in degrees and distance is in AU.
func HeliocentricEclipticJ2000(date time.Time, elements Elements) EclipticPosition {
lon, lat, distance := basic.OrbitHeliocentricEclipticJ2000(ttJulianDay(date), toBasicElements(elements))
return EclipticPosition{Lon: lon, Lat: lat, Distance: distance}
@@ -84,6 +98,7 @@ func HeliocentricEclipticJ2000(date time.Time, elements Elements) EclipticPositi
// HeliocentricEcliptic 日心历元黄道坐标 / heliocentric ecliptic coordinates of date.
//
// 返回历元黄经、黄纬和距离;角度单位度,距离单位 AU。
// Returns heliocentric ecliptic longitude, latitude, and distance of date. Angles are in degrees and distance is in AU.
func HeliocentricEcliptic(date time.Time, elements Elements) EclipticPosition {
lon, lat, distance := basic.OrbitHeliocentricEcliptic(ttJulianDay(date), toBasicElements(elements))
return EclipticPosition{Lon: lon, Lat: lat, Distance: distance}
@@ -92,6 +107,7 @@ func HeliocentricEcliptic(date time.Time, elements Elements) EclipticPosition {
// GeocentricEclipticJ2000 地心 J2000 平黄道坐标 / geocentric J2000 ecliptic coordinates.
//
// 返回黄经、黄纬和距离;角度单位度,距离单位 AU。
// Returns geocentric J2000 ecliptic longitude, latitude, and distance. Angles are in degrees and distance is in AU.
func GeocentricEclipticJ2000(date time.Time, elements Elements) EclipticPosition {
lon, lat, distance := basic.OrbitGeocentricEclipticJ2000(ttJulianDay(date), toBasicElements(elements))
return EclipticPosition{Lon: lon, Lat: lat, Distance: distance}
@@ -100,6 +116,7 @@ func GeocentricEclipticJ2000(date time.Time, elements Elements) EclipticPosition
// GeocentricEcliptic 地心历元黄道坐标 / geocentric ecliptic coordinates of date.
//
// 返回历元黄经、黄纬和距离;角度单位度,距离单位 AU。
// Returns geocentric ecliptic longitude, latitude, and distance of date. Angles are in degrees and distance is in AU.
func GeocentricEcliptic(date time.Time, elements Elements) EclipticPosition {
lon, lat, distance := basic.OrbitGeocentricEcliptic(ttJulianDay(date), toBasicElements(elements))
return EclipticPosition{Lon: lon, Lat: lat, Distance: distance}
@@ -108,6 +125,7 @@ func GeocentricEcliptic(date time.Time, elements Elements) EclipticPosition {
// GeocentricEquatorialJ2000 地心 J2000 平赤道坐标 / geocentric J2000 equatorial coordinates.
//
// 返回赤经、赤纬和距离;角度单位度,距离单位 AU。
// Returns geocentric J2000 right ascension, declination, and distance. Angles are in degrees and distance is in AU.
func GeocentricEquatorialJ2000(date time.Time, elements Elements) EquatorialPosition {
ra, dec, distance := basic.OrbitGeocentricEquatorialJ2000(ttJulianDay(date), toBasicElements(elements))
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
@@ -116,6 +134,7 @@ func GeocentricEquatorialJ2000(date time.Time, elements Elements) EquatorialPosi
// GeocentricEquatorial 地心历元平赤道坐标 / geocentric equatorial coordinates of date.
//
// 返回历元赤经、赤纬和距离;角度单位度,距离单位 AU。
// Returns geocentric right ascension, declination, and distance of date. Angles are in degrees and distance is in AU.
func GeocentricEquatorial(date time.Time, elements Elements) EquatorialPosition {
ra, dec, distance := basic.OrbitGeocentricEquatorial(ttJulianDay(date), toBasicElements(elements))
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
@@ -124,6 +143,7 @@ func GeocentricEquatorial(date time.Time, elements Elements) EquatorialPosition
// AstrometricGeocentricEquatorialJ2000 地心测算 J2000 赤道坐标 / astrometric geocentric J2000 equatorial coordinates.
//
// 返回加入光行时修正后的地心 J2000 赤经、赤纬和距离;角度单位度,距离单位 AU。
// Returns astrometric geocentric J2000 right ascension, declination, and distance after light-time correction. Angles are in degrees and distance is in AU.
func AstrometricGeocentricEquatorialJ2000(date time.Time, elements Elements) EquatorialPosition {
ra, dec, distance := basic.OrbitAstrometricGeocentricEquatorialJ2000(ttJulianDay(date), toBasicElements(elements))
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
@@ -132,6 +152,7 @@ func AstrometricGeocentricEquatorialJ2000(date time.Time, elements Elements) Equ
// ApparentGeocentricEcliptic 地心视黄道坐标 / apparent geocentric ecliptic coordinates.
//
// 返回加入光行时与章动修正后的地心视黄经、黄纬和距离;角度单位度,距离单位 AU。
// Returns apparent geocentric ecliptic longitude, latitude, and distance after light-time and nutation corrections. Angles are in degrees and distance is in AU.
func ApparentGeocentricEcliptic(date time.Time, elements Elements) EclipticPosition {
lon, lat, distance := basic.OrbitApparentGeocentricEcliptic(ttJulianDay(date), toBasicElements(elements))
return EclipticPosition{Lon: lon, Lat: lat, Distance: distance}
@@ -140,6 +161,7 @@ func ApparentGeocentricEcliptic(date time.Time, elements Elements) EclipticPosit
// ApparentGeocentricEquatorial 地心视赤道坐标 / apparent geocentric equatorial coordinates.
//
// 返回加入光行时与章动修正后的地心视赤经、赤纬和距离;角度单位度,距离单位 AU。
// Returns apparent geocentric right ascension, declination, and distance after light-time and nutation corrections. Angles are in degrees and distance is in AU.
func ApparentGeocentricEquatorial(date time.Time, elements Elements) EquatorialPosition {
ra, dec, distance := basic.OrbitApparentGeocentricEquatorial(ttJulianDay(date), toBasicElements(elements))
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
@@ -149,6 +171,7 @@ func ApparentGeocentricEquatorial(date time.Time, elements Elements) EquatorialP
//
// 返回加入光行时、章动和站心修正后的视赤经、赤纬和距离;
// `observerLon` 东经为正,`observerLat` 北纬为正,`observerHeight` 单位米。
// Returns apparent topocentric right ascension, declination, and distance after light-time, nutation, and topocentric corrections.
func ApparentTopocentricEquatorial(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) EquatorialPosition {
ra, dec, distance := basic.OrbitApparentTopocentricEquatorial(ttJulianDay(date), observerLon, observerLat, observerHeight, toBasicElements(elements))
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
@@ -157,6 +180,7 @@ func ApparentTopocentricEquatorial(date time.Time, elements Elements, observerLo
// Altitude 视高度角 / apparent altitude.
//
// 返回目标在观测者所在地的视高度角,单位度;经度东正西负,纬度北正南负,海拔单位米。
// Returns the apparent altitude of the target for the observing site, in degrees. Longitude is east-positive, latitude is north-positive, and height is in meters.
func Altitude(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
jde := basic.Date2JDE(date)
return basic.OrbitHeight(jde, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
@@ -165,6 +189,7 @@ func Altitude(date time.Time, elements Elements, observerLon, observerLat, obser
// Zenith 天顶距 / zenith distance.
//
// 返回目标在观测者所在地的天顶距,单位度。
// Returns the zenith distance of the target for the observing site, in degrees.
func Zenith(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
return 90 - Altitude(date, elements, observerLon, observerLat, observerHeight)
}
@@ -172,6 +197,7 @@ func Zenith(date time.Time, elements Elements, observerLon, observerLat, observe
// Azimuth 视方位角 / apparent azimuth.
//
// 返回目标在观测者所在地的视方位角,按正北为 0°、向东增加。
// Returns the apparent azimuth of the target for the observing site, measured from north toward east.
func Azimuth(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
jde := basic.Date2JDE(date)
return basic.OrbitAzimuth(jde, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
@@ -180,6 +206,7 @@ func Azimuth(date time.Time, elements Elements, observerLon, observerLat, observ
// HourAngle 站心视时角 / topocentric hour angle.
//
// 返回目标在观测者所在地的站心视时角,单位度。
// Returns the apparent topocentric hour angle of the target for the observing site, in degrees.
func HourAngle(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
jde := basic.Date2JDE(date)
return basic.OrbitHourAngle(jde, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
@@ -188,6 +215,7 @@ func HourAngle(date time.Time, elements Elements, observerLon, observerLat, obse
// CulminationTime 中天时刻 / culmination time.
//
// 返回目标在给定当地日期内的中天时刻,结果保持输入 `date` 的时区。
// Returns the culmination time of the target on the supplied local civil day. The result keeps the timezone of `date`.
func CulminationTime(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) time.Time {
if date.Hour() > 12 {
date = date.Add(-12 * time.Hour)
@@ -201,6 +229,7 @@ func CulminationTime(date time.Time, elements Elements, observerLon, observerLat
// RiseTime 升起时刻 / rise time.
//
// 返回目标在给定当地日期内的升起时刻;`aero=true` 时加入标准大气折射修正。
// Returns the rise time of the target on the supplied local civil day. When `aero` is true, standard atmospheric refraction is included.
func RiseTime(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64, aero bool) (time.Time, error) {
var aeroFloat float64
if aero {
@@ -218,6 +247,7 @@ func RiseTime(date time.Time, elements Elements, observerLon, observerLat, obser
// SetTime 落下时刻 / set time.
//
// 返回目标在给定当地日期内的落下时刻;`aero=true` 时加入标准大气折射修正。
// Returns the set time of the target on the supplied local civil day. When `aero` is true, standard atmospheric refraction is included.
func SetTime(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64, aero bool) (time.Time, error) {
var aeroFloat float64
if aero {
+1
View File
@@ -9,6 +9,7 @@ import (
// ParallacticAngle 轨道目标视差角(天顶方向角) / orbit-target parallactic angle.
//
// 返回轨道目标在观测者所在地的视差角,单位度;`observerLon` 东经为正,`observerLat` 北纬为正,`observerHeight` 单位米。
// Returns the parallactic angle of the orbital target for the observing site, in degrees. `observerLon` is east-positive, `observerLat` is north-positive, and `observerHeight` is in meters.
func ParallacticAngle(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
position := ApparentTopocentricEquatorial(date, elements, observerLon, observerLat, observerHeight)
return basic.ParallacticAngleByHourAngle(
+6 -2
View File
@@ -8,7 +8,10 @@ import (
const visualBinaryDeg = 180 / math.Pi
const visualBinaryRad = math.Pi / 180
// VisualBinaryElements 视双星轨道要素,采用《天文算法》第 55 章的经典口径。
// VisualBinaryElements 视双星轨道要素 / visual-binary orbital elements.
//
// 采用《天文算法》第 55 章的经典口径。
// Uses the classical convention described in Chapter 55 of Astronomical Algorithms.
type VisualBinaryElements struct {
PeriodYears float64 // 周期 P,单位平太阳年 / orbital period in mean solar years.
PeriastronYear float64 // 过近星点时刻 T,采用带小数的年 / epoch of periastron as a decimal year.
@@ -19,7 +22,7 @@ type VisualBinaryElements struct {
PeriastronArgument float64 // 近星点角距 ω,单位度 / argument of periastron in degrees.
}
// VisualBinaryPosition 视双星在天球上的计算结果
// VisualBinaryPosition 视双星在天球上的计算结果 / computed sky-plane position of a visual binary.
type VisualBinaryPosition struct {
Year float64 // 计算使用的小数年 / decimal year used for the evaluation.
MeanAnomaly float64 // 平近点角 M,单位度 / mean anomaly in degrees.
@@ -41,6 +44,7 @@ func VisualBinary(date time.Time, elements VisualBinaryElements) VisualBinaryPos
// VisualBinaryByYear 视双星位置(按小数年) / visual binary position by decimal year.
//
// 返回给定小数年对应的视双星位置角和角距离。
// Returns the position angle and apparent separation for the supplied decimal year.
func VisualBinaryByYear(year float64, elements VisualBinaryElements) VisualBinaryPosition {
if !validVisualBinaryElements(year, elements) {
return invalidVisualBinaryPosition(year)
+1 -1
View File
@@ -25,7 +25,7 @@ func WherePlanetN(xt, zn int, jd float64, n int) float64 {
t := (jd - 2451545) / 36525.0000
t /= 10 // 转为儒略千年数
body := planetViews[xt]
body := planetViews()[xt]
coord := body.coords[zn]
baseOrderTerms := len(coord.orders[0])
+14 -1
View File
@@ -25,8 +25,9 @@ func TestWherePlanetNFullMatchesDefault(t *testing.T) {
}
func TestPlanetViewsMatchRawCuts(t *testing.T) {
views := planetViews()
for bodyIndex, raw := range planetRawData {
view := planetViews[bodyIndex]
view := views[bodyIndex]
if math.Float64bits(view.scale) != math.Float64bits(raw[0]) {
t.Fatalf("body=%d scale mismatch", bodyIndex)
}
@@ -42,3 +43,15 @@ func TestPlanetViewsMatchRawCuts(t *testing.T) {
}
}
}
func TestBuildPlanetViewsRejectsInvalidCuts(t *testing.T) {
_, err := buildPlanetViews([][]float64{{
10000000000,
20, 21, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20,
}})
if err == nil {
t.Fatal("expected invalid cut error")
}
}
+23 -6
View File
@@ -1,6 +1,9 @@
package planet
import "fmt"
import (
"fmt"
"sync"
)
type coordSeriesView struct {
orders [6][]float64
@@ -11,13 +14,27 @@ type planetView struct {
coords [3]coordSeriesView
}
var planetViews = buildPlanetViews(planetRawData)
var (
planetViewsOnce sync.Once
planetViewsCache []planetView
planetViewsErr error
)
func buildPlanetViews(rawData [][]float64) []planetView {
func planetViews() []planetView {
planetViewsOnce.Do(func() {
planetViewsCache, planetViewsErr = buildPlanetViews(planetRawData)
})
if planetViewsErr != nil {
panic(planetViewsErr)
}
return planetViewsCache
}
func buildPlanetViews(rawData [][]float64) ([]planetView, error) {
views := make([]planetView, len(rawData))
for bodyIndex, raw := range rawData {
if len(raw) < 20 {
panic(fmt.Sprintf("planet raw data %d too short: %d", bodyIndex, len(raw)))
return nil, fmt.Errorf("planet raw data %d too short: %d", bodyIndex, len(raw))
}
view := planetView{scale: raw[0]}
for zn := 0; zn < 3; zn++ {
@@ -26,12 +43,12 @@ func buildPlanetViews(rawData [][]float64) []planetView {
start := int(raw[pn+order])
end := int(raw[pn+order+1])
if start < 0 || end < start || end > len(raw) {
panic(fmt.Sprintf("planet raw data %d coord %d order %d invalid cut: %d..%d (len=%d)", bodyIndex, zn, order, start, end, len(raw)))
return nil, fmt.Errorf("planet raw data %d coord %d order %d invalid cut: %d..%d (len=%d)", bodyIndex, zn, order, start, end, len(raw))
}
view.coords[zn].orders[order] = raw[start:end]
}
}
views[bodyIndex] = view
}
return views
return views, nil
}
+24 -24
View File
@@ -207,8 +207,8 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
// LastConjunction 上一次合日 / previous conjunction with the Sun.
//
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or before date, keeping date's time zone.
func LastConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnConjunction(jde), date.Location(), false)
@@ -216,8 +216,8 @@ func LastConjunction(date time.Time) time.Time {
// NextConjunction 下一次合日 / next conjunction with the Sun.
//
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or after date, keeping date's time zone.
func NextConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnConjunction(jde), date.Location(), false)
@@ -225,8 +225,8 @@ func NextConjunction(date time.Time) time.Time {
// LastOpposition 上一次冲日 / previous opposition.
//
// 返回 date 之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the most recent opposition relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or before date, keeping date's time zone.
func LastOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnOpposition(jde), date.Location(), false)
@@ -234,8 +234,8 @@ func LastOpposition(date time.Time) time.Time {
// NextOpposition 下一次冲日 / next opposition.
//
// 返回 date 之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the next opposition relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or after date, keeping date's time zone.
func NextOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnOpposition(jde), date.Location(), false)
@@ -243,8 +243,8 @@ func NextOpposition(date time.Time) time.Time {
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
//
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from prograde to retrograde, keeping date's time zone.
func LastProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnProgradeToRetrograde(jde), date.Location(), false)
@@ -252,8 +252,8 @@ func LastProgradeToRetrograde(date time.Time) time.Time {
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
//
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from prograde to retrograde, keeping date's time zone.
func NextProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnProgradeToRetrograde(jde), date.Location(), false)
@@ -261,8 +261,8 @@ func NextProgradeToRetrograde(date time.Time) time.Time {
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
//
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from retrograde to prograde, keeping date's time zone.
func LastRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnRetrogradeToPrograde(jde), date.Location(), false)
@@ -270,8 +270,8 @@ func LastRetrogradeToPrograde(date time.Time) time.Time {
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
//
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from retrograde to prograde, keeping date's time zone.
func NextRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnRetrogradeToPrograde(jde), date.Location(), false)
@@ -279,8 +279,8 @@ func NextRetrogradeToPrograde(date time.Time) time.Time {
// LastEasternQuadrature 上一次东方照 / previous eastern quadrature.
//
// 返回 date 之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the most recent eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or before date, keeping date's time zone.
func LastEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnEasternQuadrature(jde), date.Location(), false)
@@ -288,8 +288,8 @@ func LastEasternQuadrature(date time.Time) time.Time {
// NextEasternQuadrature 下一次东方照 / next eastern quadrature.
//
// 返回 date 之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the next eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or after date, keeping date's time zone.
func NextEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnEasternQuadrature(jde), date.Location(), false)
@@ -297,8 +297,8 @@ func NextEasternQuadrature(date time.Time) time.Time {
// LastWesternQuadrature 上一次西方照 / previous western quadrature.
//
// 返回 date 之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the most recent western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or before date, keeping date's time zone.
func LastWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnWesternQuadrature(jde), date.Location(), false)
@@ -306,8 +306,8 @@ func LastWesternQuadrature(date time.Time) time.Time {
// NextWesternQuadrature 下一次西方照 / next western quadrature.
//
// 返回 date 之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the next western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or after date, keeping date's time zone.
func NextWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnWesternQuadrature(jde), date.Location(), false)
+2
View File
@@ -10,6 +10,8 @@ import (
//
// ra/dec 为瞬时赤经赤纬,单位度;lon/lat 为观测者经纬度,东正西负、北正南负。
// 返回值为有符号视差角,单位度。
// ra/dec are apparent equatorial coordinates in degrees; lon/lat are east-positive and north-positive.
// Returns the signed parallactic angle in degrees.
func ParallacticAngle(date time.Time, ra, dec, lon, lat float64) float64 {
jde := basic.Date2JDE(date)
_, loc := date.Zone()
+2
View File
@@ -100,6 +100,8 @@ func DownTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, e
// DownTimeN 截断项日落时刻别名 / deprecated truncated sunset alias.
//
// Deprecated: use SetTimeN instead.
//
// 参数与 SetTimeN 相同,仅为兼容旧接口保留。
// Same as SetTimeN and kept only for backward compatibility.
func DownTimeN(date time.Time, lon, lat, height float64, aero bool, n int) (time.Time, error) {
+43 -1
View File
@@ -15,6 +15,12 @@ import (
//
// 坐标系沿本章通用约定:x 轴位于盘面内且保持水平,y 轴沿盘面最大坡度向上,
// x 正向为右侧,y 正向为上坡方向。
// Latitude is geographic latitude, north positive. PlaneNormalAzimuth is the azimuth of the dial-plane normal,
// measured from north toward east. PlaneNormalZenithDistance is the zenith distance of the plane normal:
// 0 degrees for a horizontal dial and 90 degrees for a vertical dial. StylusLength is the length of the direct stylus normal to the plane.
//
// The plane coordinates follow the chapter convention: the x axis lies in the plane and remains horizontal,
// the y axis follows the steepest upward direction in the plane, x positive points to the right, and y positive points upslope.
type PlanarDial struct {
Latitude float64
PlaneNormalAzimuth float64
@@ -27,6 +33,9 @@ type PlanarDial struct {
// X/Y 为影尖在盘面坐标系中的坐标;DenominatorQ 对应书中公式里的 Q
// SunAboveHorizon 表示太阳在地平线上方;PlaneIlluminated 表示盘面被太阳照亮;
// Illuminated 为前两者同时满足。
// X/Y are the shadow-tip coordinates in the dial-plane coordinate system. DenominatorQ is the Q term from the book formula.
// SunAboveHorizon reports whether the Sun is above the horizon. PlaneIlluminated reports whether sunlight reaches the dial plane.
// Illuminated is true only when both conditions are satisfied.
type PlanarShadowPoint struct {
X float64
Y float64
@@ -41,6 +50,9 @@ type PlanarShadowPoint struct {
// CenterX/CenterY 为日晷中心(极轴晷针固定点)坐标;PolarStylusLength 为极轴晷针长度;
// PolarStylusPlaneAngle 为极轴晷针与盘面的夹角。HasFiniteCenter 为 false 时,
// 表示极轴晷针与盘面平行,中心退化到无穷远处。
// CenterX/CenterY are the coordinates of the dial center, where the polar stylus is fixed. PolarStylusLength is the polar-stylus length.
// PolarStylusPlaneAngle is the angle between the polar stylus and the dial plane. When HasFiniteCenter is false,
// the polar stylus is parallel to the plane and the center degenerates to infinity.
type PlanarGeometry struct {
CenterX float64
CenterY float64
@@ -53,6 +65,8 @@ type PlanarGeometry struct {
//
// Start/End 均为有符号太阳时角,单位度,满足 Start <= End。
// 约定取值范围为 [-180, 180],用于表达一天中的一段连续时角。
// Start and End are signed solar hour angles in degrees, with Start <= End.
// The intended range is [-180, 180], representing one continuous hour-angle span within a day.
type HourAngleInterval struct {
Start float64
End float64
@@ -61,6 +75,7 @@ type HourAngleInterval struct {
// DeclinationCurveSample 赤纬曲线采样点 / sampled point on a declination curve.
//
// HourAngle 为采样点的太阳时角;Point 为该时角下的影尖位置与照明状态。
// HourAngle is the solar hour angle of the sample. Point gives the shadow-tip position and illumination state at that hour angle.
type DeclinationCurveSample struct {
HourAngle float64
Point PlanarShadowPoint
@@ -69,6 +84,7 @@ type DeclinationCurveSample struct {
// DeclinationCurveSegment 赤纬曲线分段 / one illuminated segment of a declination curve.
//
// Interval 给出该段对应的连续受光时角范围;Samples 为该段内部的采样点。
// Interval gives the continuous illuminated hour-angle span for the segment. Samples contains the sampled points within that segment.
type DeclinationCurveSegment struct {
Declination float64
Interval HourAngleInterval
@@ -80,6 +96,9 @@ type DeclinationCurveSegment struct {
// Date 为该采样点对应的绝对时刻;其日期来自输入 date,钟面时间由调用参数指定。
// Declination 为该采样瞬时的太阳赤纬;HourAngle 为换算后的视太阳时角;
// Point 为该时角下的影尖位置与照明状态。
// Date is the absolute instant of the sample. Its date comes from the input date, while the clock reading comes from the call parameters.
// Declination is the solar declination at that instant. HourAngle is the derived apparent-solar hour angle.
// Point gives the shadow-tip position and illumination state at that hour angle.
type TimeLineSample struct {
Date time.Time
Declination float64
@@ -90,6 +109,7 @@ type TimeLineSample struct {
// EquatorialNorthDial 北面赤道日晷 / north-face equatorial dial.
//
// 北半球时用于春夏半年(太阳赤纬为正);南半球也可直接按公式使用。
// In the northern hemisphere this face is used during the spring and summer half-year, when solar declination is positive. The same formula can also be used directly in the southern hemisphere.
func EquatorialNorthDial(latitude, stylusLength float64) PlanarDial {
return PlanarDial{
Latitude: latitude,
@@ -102,6 +122,7 @@ func EquatorialNorthDial(latitude, stylusLength float64) PlanarDial {
// EquatorialSouthDial 南面赤道日晷 / south-face equatorial dial.
//
// 北半球时用于秋冬半年(太阳赤纬为负);南半球也可直接按公式使用。
// In the northern hemisphere this face is used during the autumn and winter half-year, when solar declination is negative. The same formula can also be used directly in the southern hemisphere.
func EquatorialSouthDial(latitude, stylusLength float64) PlanarDial {
return PlanarDial{
Latitude: latitude,
@@ -114,6 +135,7 @@ func EquatorialSouthDial(latitude, stylusLength float64) PlanarDial {
// HorizontalDial 水平日晷 / horizontal dial.
//
// 该构造器采用经典水平日晷的坐标约定:x 轴向东,y 轴向北。
// This constructor follows the classical horizontal-dial coordinate convention: the x axis points east and the y axis points north.
func HorizontalDial(latitude, stylusLength float64) PlanarDial {
return PlanarDial{
Latitude: latitude,
@@ -127,6 +149,8 @@ func HorizontalDial(latitude, stylusLength float64) PlanarDial {
//
// planeNormalAzimuth 为盘面法线方位角,按正北为 0°、向东增加。
// 例如:朝南墙面取 180°,朝东墙面取 90°。
// planeNormalAzimuth is the azimuth of the plane normal, measured from north toward east.
// For example, use 180 degrees for a south-facing wall and 90 degrees for an east-facing wall.
func VerticalDial(latitude, planeNormalAzimuth, stylusLength float64) PlanarDial {
return PlanarDial{
Latitude: latitude,
@@ -136,7 +160,10 @@ func VerticalDial(latitude, planeNormalAzimuth, stylusLength float64) PlanarDial
}
}
// Geometry 返回平面日晷中心与极轴晷针几何量 / returns the derived planar geometry.
// Geometry 平面日晷中心与极轴晷针几何量 / derived planar-dial center and polar-stylus geometry.
//
// 返回平面日晷的中心与极轴晷针几何量。
// Returns the derived center and polar-stylus geometry of the planar dial.
func (dial PlanarDial) Geometry() PlanarGeometry {
geometry := PlanarGeometry{
CenterX: math.NaN(),
@@ -167,6 +194,7 @@ func (dial PlanarDial) Geometry() PlanarGeometry {
// ShadowPointByHourAngleDeclination 影尖坐标(按时角与赤纬) / shadow point from hour angle and declination.
//
// hourAngle 为有符号太阳时角,上午为负,下午为正;declination 为太阳赤纬,单位度。
// hourAngle is the signed solar hour angle, negative in the morning and positive in the afternoon. declination is solar declination in degrees.
func (dial PlanarDial) ShadowPointByHourAngleDeclination(hourAngle, declination float64) PlanarShadowPoint {
point := PlanarShadowPoint{
X: math.NaN(),
@@ -194,6 +222,7 @@ func (dial PlanarDial) ShadowPointByHourAngleDeclination(hourAngle, declination
// ShadowPointAt 影尖坐标(按绝对时刻) / shadow point at an instant.
//
// 直接读取该时刻对应的视太阳时角和瞬时太阳赤纬,并返回平面日晷上的影尖位置。
// Uses the apparent-solar hour angle and instantaneous solar declination at the supplied instant, and returns the shadow-tip position on the planar dial.
func (dial PlanarDial) ShadowPointAt(date time.Time, lon float64) PlanarShadowPoint {
return dial.ShadowPointByHourAngleDeclination(HourAngle(date, lon), sun.ApparentDec(date))
}
@@ -202,6 +231,8 @@ func (dial PlanarDial) ShadowPointAt(date time.Time, lon float64) PlanarShadowPo
//
// date 应处于目标地点的地方平太阳时区,例如 `MeanSolarTime` 的返回值;其原有钟面时间会被忽略。
// meanSolarHours 为地方平太阳时钟面读数,单位小时,例如 9.5 表示 09:30。
// date should already be expressed in the local mean-solar timezone of the site, for example a value returned by `MeanSolarTime`; its original clock fields are ignored.
// meanSolarHours is the local mean-solar clock reading in hours, for example 9.5 for 09:30.
func (dial PlanarDial) MeanSolarTimePoint(date time.Time, meanSolarHours float64) PlanarShadowPoint {
sampleTime := dateWithClockHours(date, meanSolarHours)
declination := sun.ApparentDec(sampleTime)
@@ -211,6 +242,7 @@ func (dial PlanarDial) MeanSolarTimePoint(date time.Time, meanSolarHours float64
// ZoneTimePoint 区时影尖位置 / shadow point for zone time.
//
// date 提供民用日期和时区,原有钟面时间会被忽略;zoneTimeHours 为该时区下的区时钟面读数。
// date provides the civil date and timezone; its original clock fields are ignored. zoneTimeHours is the civil clock reading in that timezone.
func (dial PlanarDial) ZoneTimePoint(date time.Time, lon, zoneTimeHours float64) PlanarShadowPoint {
sampleTime := dateWithClockHours(date, zoneTimeHours)
declination := sun.ApparentDec(sampleTime)
@@ -221,6 +253,8 @@ func (dial PlanarDial) ZoneTimePoint(date time.Time, lon, zoneTimeHours float64)
//
// dates 由调用者自行决定取样日期密度,例如每月或每日;每个 date 都应处于目标地点的地方平太阳时区,
// 例如先通过 `MeanSolarTime` 得到对应地点的地方平太阳时再取其年月日。meanSolarHours 为地方平太阳时钟面读数。
// dates define the sampling cadence, for example monthly or daily. Each date should already be in the site's local mean-solar timezone,
// for example by first calling `MeanSolarTime` and then keeping its year, month, and day. meanSolarHours is the local mean-solar clock reading.
func (dial PlanarDial) MeanSolarTimeLine(dates []time.Time, meanSolarHours float64) []TimeLineSample {
if !isFinite(meanSolarHours) {
return nil
@@ -244,6 +278,8 @@ func (dial PlanarDial) MeanSolarTimeLine(dates []time.Time, meanSolarHours float
//
// dates 由调用者自行决定取样日期密度;zoneTimeHours 为 date 所在时区的区时钟面读数。
// 每个 date 的原有钟面时间都会被 zoneTimeHours 替换。
// dates define the sampling cadence. zoneTimeHours is the civil clock reading in the timezone carried by each date.
// The original clock fields of every date are replaced by zoneTimeHours.
func (dial PlanarDial) ZoneTimeLine(dates []time.Time, lon, zoneTimeHours float64) []TimeLineSample {
if !isFinite(zoneTimeHours) || !isFinite(lon) {
return nil
@@ -266,6 +302,7 @@ func (dial PlanarDial) ZoneTimeLine(dates []time.Time, lon, zoneTimeHours float6
// PlaneIlluminatedHourAngleIntervals 盘面受光时角区间 / plane-illuminated hour-angle intervals.
//
// declination 为太阳赤纬,单位度。返回的区间只考虑盘面受光,不判断太阳是否在地平线上方。
// declination is solar declination in degrees. The returned intervals consider only whether the dial plane is illuminated and do not test whether the Sun is above the horizon.
func (dial PlanarDial) PlaneIlluminatedHourAngleIntervals(declination float64) []HourAngleInterval {
if !dial.isFinite() || !isFinite(declination) {
return nil
@@ -277,6 +314,7 @@ func (dial PlanarDial) PlaneIlluminatedHourAngleIntervals(declination float64) [
// IlluminatedHourAngleIntervals 可见且受光时角区间 / illuminated hour-angle intervals.
//
// declination 为太阳赤纬,单位度。结果可直接用于日晷绘图时筛掉无效时线。
// declination is solar declination in degrees. The result can be used directly to discard invalid hour-line segments in sundial plotting.
func (dial PlanarDial) IlluminatedHourAngleIntervals(declination float64) []HourAngleInterval {
aboveHorizon := SunAboveHorizonHourAngleIntervals(dial.Latitude, declination)
planeLit := dial.PlaneIlluminatedHourAngleIntervals(declination)
@@ -287,6 +325,8 @@ func (dial PlanarDial) IlluminatedHourAngleIntervals(declination float64) []Hour
//
// declination 为太阳赤纬,单位度;hourAngleStep 为采样步长,单位度,常用值是 15°(每小时一格)。
// 返回值按受光区间分段,每段都带有精确的时角范围;Samples 只包含区间内部的有效采样点。
// declination is solar declination in degrees. hourAngleStep is the sampling step in degrees; 15 degrees is a common one-hour spacing.
// The return value is split by illuminated intervals. Each segment carries the exact hour-angle bounds, and Samples contains only valid interior sample points.
func (dial PlanarDial) DeclinationCurve(declination, hourAngleStep float64) []DeclinationCurveSegment {
if !dial.isFinite() || !isFinite(declination) || !isFinite(hourAngleStep) || hourAngleStep <= 0 {
return nil
@@ -315,6 +355,7 @@ func (dial PlanarDial) DeclinationCurve(declination, hourAngleStep float64) []De
// DeclinationCurveAt 瞬时赤纬曲线采样 / declination-curve samples at an instant.
//
// 用 date 对应瞬时太阳赤纬生成日晷分段曲线采样。
// Builds the segmented declination-curve samples from the instantaneous solar declination at date.
func (dial PlanarDial) DeclinationCurveAt(date time.Time, hourAngleStep float64) []DeclinationCurveSegment {
return dial.DeclinationCurve(sun.ApparentDec(date), hourAngleStep)
}
@@ -323,6 +364,7 @@ func (dial PlanarDial) DeclinationCurveAt(date time.Time, hourAngleStep float64)
//
// latitude 为地理纬度,declination 为太阳赤纬,单位度。结果只反映太阳是否升到地平线上方,
// 不包含盘面朝向的影响。
// latitude is geographic latitude and declination is solar declination, both in degrees. The result reflects only whether the Sun is above the horizon and does not include dial-plane orientation.
func SunAboveHorizonHourAngleIntervals(latitude, declination float64) []HourAngleInterval {
if !isFinite(latitude) || !isFinite(declination) {
return nil
+7
View File
@@ -39,6 +39,9 @@ func HourAngle(date time.Time, lon float64) float64 {
// date 负责提供地方平太阳时日期与时区,原有钟面时间会被 meanSolarHours 替换;
// meanSolarHours 为地方平太阳时钟面读数,单位小时,例如 9.5 表示地方平太阳时 09:30。
// 返回对应的视太阳时角,单位度,上午为负,下午为正。
// date provides the local mean-solar date and timezone, while its clock fields are replaced by meanSolarHours.
// meanSolarHours is the local mean-solar clock reading in hours, for example 9.5 for 09:30.
// Returns the corresponding apparent-solar hour angle in degrees, negative in the morning and positive in the afternoon.
func MeanSolarHourAngle(date time.Time, meanSolarHours float64) float64 {
if !isFinite(meanSolarHours) {
return math.NaN()
@@ -52,6 +55,9 @@ func MeanSolarHourAngle(date time.Time, meanSolarHours float64) float64 {
// zoneTimeHours 为 date 所在时区下的钟面读数,单位小时;lon 为当地经度,东正西负。
// 返回该区时在给定经度上对应的视太阳时角,单位度,上午为负,下午为正。
// date 提供民用日期和时区;其原有钟面时间会被 zoneTimeHours 替换。
// zoneTimeHours is the civil clock reading in the timezone carried by date, in hours; lon is east-positive longitude.
// Returns the apparent-solar hour angle for that civil time and longitude, in degrees, negative in the morning and positive in the afternoon.
// date provides the civil date and timezone, and its original clock fields are replaced by zoneTimeHours.
func ZoneTimeHourAngle(date time.Time, lon, zoneTimeHours float64) float64 {
if !isFinite(zoneTimeHours) || !isFinite(lon) {
return math.NaN()
@@ -79,6 +85,7 @@ func HorizontalHourLineAngle(lat, hourAngle float64) float64 {
// HorizontalHourLineAngleAt 水平日晷时线角 / horizontal sundial hour-line angle.
//
// 先按给定时刻和经度求瞬时视太阳时角,再结合纬度返回水平日晷的时线角。
// First derives the apparent-solar hour angle for the supplied instant and longitude, then returns the horizontal-dial hour-line angle for the given latitude.
func HorizontalHourLineAngleAt(date time.Time, lon, lat float64) float64 {
return HorizontalHourLineAngle(lat, HourAngle(date, lon))
}
+24 -24
View File
@@ -207,8 +207,8 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
// LastConjunction 上一次合日 / previous conjunction with the Sun.
//
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or before date, keeping date's time zone.
func LastConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastUranusConjunction(jde), date.Location(), false)
@@ -216,8 +216,8 @@ func LastConjunction(date time.Time) time.Time {
// NextConjunction 下一次合日 / next conjunction with the Sun.
//
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or after date, keeping date's time zone.
func NextConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextUranusConjunction(jde), date.Location(), false)
@@ -225,8 +225,8 @@ func NextConjunction(date time.Time) time.Time {
// LastOpposition 上一次冲日 / previous opposition.
//
// 返回 date 之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the most recent opposition relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or before date, keeping date's time zone.
func LastOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastUranusOpposition(jde), date.Location(), false)
@@ -234,8 +234,8 @@ func LastOpposition(date time.Time) time.Time {
// NextOpposition 下一次冲日 / next opposition.
//
// 返回 date 之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the next opposition relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or after date, keeping date's time zone.
func NextOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextUranusOpposition(jde), date.Location(), false)
@@ -243,8 +243,8 @@ func NextOpposition(date time.Time) time.Time {
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
//
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from prograde to retrograde, keeping date's time zone.
func LastProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastUranusProgradeToRetrograde(jde), date.Location(), false)
@@ -252,8 +252,8 @@ func LastProgradeToRetrograde(date time.Time) time.Time {
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
//
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from prograde to retrograde, keeping date's time zone.
func NextProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextUranusProgradeToRetrograde(jde), date.Location(), false)
@@ -261,8 +261,8 @@ func NextProgradeToRetrograde(date time.Time) time.Time {
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
//
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from retrograde to prograde, keeping date's time zone.
func LastRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastUranusRetrogradeToPrograde(jde), date.Location(), false)
@@ -270,8 +270,8 @@ func LastRetrogradeToPrograde(date time.Time) time.Time {
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
//
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from retrograde to prograde, keeping date's time zone.
func NextRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextUranusRetrogradeToPrograde(jde), date.Location(), false)
@@ -279,8 +279,8 @@ func NextRetrogradeToPrograde(date time.Time) time.Time {
// LastEasternQuadrature 上一次东方照 / previous eastern quadrature.
//
// 返回 date 之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the most recent eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or before date, keeping date's time zone.
func LastEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastUranusEasternQuadrature(jde), date.Location(), false)
@@ -288,8 +288,8 @@ func LastEasternQuadrature(date time.Time) time.Time {
// NextEasternQuadrature 下一次东方照 / next eastern quadrature.
//
// 返回 date 之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the next eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or after date, keeping date's time zone.
func NextEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextUranusEasternQuadrature(jde), date.Location(), false)
@@ -297,8 +297,8 @@ func NextEasternQuadrature(date time.Time) time.Time {
// LastWesternQuadrature 上一次西方照 / previous western quadrature.
//
// 返回 date 之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the most recent western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or before date, keeping date's time zone.
func LastWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastUranusWesternQuadrature(jde), date.Location(), false)
@@ -306,8 +306,8 @@ func LastWesternQuadrature(date time.Time) time.Time {
// NextWesternQuadrature 下一次西方照 / next western quadrature.
//
// 返回 date 之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the next western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or after date, keeping date's time zone.
func NextWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextUranusWesternQuadrature(jde), date.Location(), false)
+73
View File
@@ -0,0 +1,73 @@
package venus
import (
"time"
"b612.me/astro/basic"
)
// TransitInfo 地心金星凌日信息 / geocentric Venus transit information.
//
// Start、Greatest、End、InternalStart、InternalEnd 都保持调用者输入的时区。
// 内切接触不存在时 InternalStart / InternalEnd 为零值。
// Start, Greatest, End, InternalStart, and InternalEnd preserve the caller's timezone.
// InternalStart and InternalEnd are zero values when internal contacts do not exist.
type TransitInfo struct {
Valid bool
Start time.Time
InternalStart time.Time
Greatest time.Time
InternalEnd time.Time
End time.Time
Duration time.Duration
InternalDuration time.Duration
MinimumSeparationArcsec float64
SunSemidiameterArcsec float64
PlanetSemidiameterArcsec float64
HasInternal bool
}
// NextTransit 下一次地心金星凌日 / next geocentric Venus transit.
func NextTransit(date time.Time) TransitInfo {
return transitInfoFromBasic(basic.NextVenusTransit(basic.Date2JDE(date.UTC())), date.Location())
}
// LastTransit 上一次地心金星凌日 / previous geocentric Venus transit.
func LastTransit(date time.Time) TransitInfo {
return transitInfoFromBasic(basic.LastVenusTransit(basic.Date2JDE(date.UTC())), date.Location())
}
// ClosestTransit 最近一次地心金星凌日 / closest geocentric Venus transit.
func ClosestTransit(date time.Time) TransitInfo {
return transitInfoFromBasic(basic.ClosestVenusTransit(basic.Date2JDE(date.UTC())), date.Location())
}
func transitInfoFromBasic(result basic.PlanetTransitResult, loc *time.Location) TransitInfo {
if !result.Valid {
return TransitInfo{}
}
start := basic.JDE2DateByZone(result.ExternalIngress, loc, false)
greatest := basic.JDE2DateByZone(result.Greatest, loc, false)
end := basic.JDE2DateByZone(result.ExternalEgress, loc, false)
info := TransitInfo{
Valid: true,
Start: start,
Greatest: greatest,
End: end,
Duration: end.Sub(start),
MinimumSeparationArcsec: result.MinimumSeparationArcsec,
SunSemidiameterArcsec: result.SunSemidiameterArcsec,
PlanetSemidiameterArcsec: result.PlanetSemidiameterArcsec,
HasInternal: result.HasInternal,
}
if result.HasInternal {
info.InternalStart = basic.JDE2DateByZone(result.InternalIngress, loc, false)
info.InternalEnd = basic.JDE2DateByZone(result.InternalEgress, loc, false)
info.InternalDuration = info.InternalEnd.Sub(info.InternalStart)
}
return info
}
+23
View File
@@ -0,0 +1,23 @@
package venus
import (
"testing"
"time"
)
func TestTransitWrappers(t *testing.T) {
loc := time.FixedZone("CST", 8*3600)
info := NextTransit(time.Date(2012, 1, 1, 0, 0, 0, 0, loc))
if !info.Valid {
t.Fatal("expected valid transit")
}
if info.Greatest.Location() != loc {
t.Fatalf("timezone mismatch: got %v want %v", info.Greatest.Location(), loc)
}
if info.Greatest.Year() != 2012 || info.Greatest.Month() != time.June || info.Greatest.Day() != 6 {
t.Fatalf("unexpected greatest time: %s", info.Greatest)
}
if !info.HasInternal || info.Duration <= 0 || info.InternalDuration <= 0 {
t.Fatalf("unexpected durations: %+v", info)
}
}

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