2 Commits

Author SHA1 Message Date
b612 0e3825d5ba test: 修正日食中心阴影边界测试的时标换算
- 将日下点计算由 TT2UTC 改为 TT2UT1,使测试与地球自转几何口径一致
2026-10-16 07:52:20 +08:00
b612 044f2f4381 feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
2026-10-15 23:54:15 +08:00
675 changed files with 62984 additions and 8397 deletions
+160
View File
@@ -0,0 +1,160 @@
本文件是第三方数据与条款声明。代码与文档采用 Apache-2.0;下列数据文件**不适用** Apache-2.0,各自遵循其来源条款。分发内嵌了这些数据的二进制(含 c-shared DLL/SO)时,须一并附上对应署名。
This file is the per-component licence and attribution notice. The code and documentation are Apache-2.0; the data files listed below are not, and each follows the terms of its own source. Distributing a binary that embeds them — including the c-shared DLL/SO build — requires carrying the corresponding attribution.
================================================================================
1. SELENE (KAGUYA) lunar limb profile — moon/limb/kaguya_lalt.bin
================================================================================
Source data
SELENE Global Grid Topographic Data of the Moon (LALT_GGT_NUM), dataset
sln-l-lalt-5-topo-ggt-num-v2.0, version 2.0, published 2017.
Origin of the data: ISAS/JAXA.
Provided by the SELENE (KAGUYA) Data Archive (DARTS), ISAS/JAXA:
https://darts.isas.jaxa.jp/datasets/darts:sln-l-lalt-5-topo-ggt-num-v2.0
https://data.darts.isas.jaxa.jp/pub/pds3/sln-l-lalt-5-topo-ggt-num-v2.0/
Acknowledgement, as requested by the SELENE Data Archive
"We thank the SELENE(KAGUYA) LALT team and the SELENE Data Archive for
providing the SELENE(KAGUYA) data. SELENE is a Japanese mission developed
and operated by JAXA."
Modifications
The original PDS3 fixed-width table (1/16 degree global grid, 2880 x 5760
samples) was converted to a float32 elevation grid and then resampled into a
limb-radius profile table: the limb height is tabulated on a 1 degree grid of
selenographic (libration) longitude and latitude and on a 0.1 degree grid of
position angle, taking max (R + h) cos(alpha) over a +/-9 degree arc for each
direction. Values are quantized to 4 m, delta-encoded along longitude and
latitude, and then stored in this project's own zlib-compressed LLB1
container. No other changes were made.
Licence
The source data are provided under the ISAS Data Policy,
https://www.isas.jaxa.jp/en/researchers/data-policy/
which states that its rules are compatible with Creative Commons Attribution
4.0 International, https://creativecommons.org/licenses/by/4.0/
This file is redistributed under the ISAS Data Policy. It is provided "as
is", without warranties of any kind. ISAS/JAXA is not responsible for the
actions of data users and does not endorse this project.
Embedding
Compiled into the moon/limb package with //go:embed, and linked only when
that package is imported. A copy of this section ships as moon/limb/NOTICE
so that vendored copies of the package keep the attribution.
================================================================================
2. Planetary and lunar theories — planet/internal/semianalytic/**/*.bin
================================================================================
Upstream theories and authors
VSOP2013 — planetary theory, Simon, Francou, Fienga & Manche (2013),
distributed by IMCCE at https://ftp.imcce.fr/pub/ephem/planets/vsop2013/
TOP2013 — planetary theory for the outer planets, IMCCE, distributed at
https://ftp.imcce.fr/pub/ephem/planets/top2013/
ELP/MPP02 — lunar theory, J. Chapront, M. Chapront & G. Francou, 2003,
distributed by SYRTE, Observatoire de Paris (not through IMCCE's ftp server)
at ftp://cyrano-se.obspm.fr/pub/2_lunar_solutions/2_elpmpp02/
Its distribution README.TXT (December 2002) is purely scientific — authors,
description, file list and references — and states no licence and no terms
of use.
Used by the legacy model as well:
ELP2000-82B — earlier lunar solution, Chapront-Touzé & Chapront, distributed
by IMCCE at https://ftp.imcce.fr/pub/ephem/moon/elp82b/
VSOP87 — earlier planetary series, Bretagnon & Francou (1988).
What this project distributes
Its own numerical refits of those theories' argument structure to the JPL
DE440/DE441 ephemerides. The amplitudes in the *.bin tables are produced by
this project's fitting pipeline; the upstream coefficient files are not
redistributed here. Relative to the upstream theories the tables are
modified in two ways: (1) the argument structure is truncated to the terms
the fit retains, and (2) every amplitude is re-fitted to DE440/DE441 instead
of being taken from the published series.
Published constants
The argument phases and frequencies (the VSOP2013 "ll(1..17)" set) and the
GM constants used by the generator are published in the VSOP2013 README.pdf
distributed by IMCCE and in Simon, Francou, Fienga & Manche, A&A 557, A49
(2013); for the Moon, in Chapront & Francou, A&A 404, 735 (2003).
Rights in the underlying theories
This project can neither allow nor forbid the use of the VSOP2013, TOP2013,
ELP/MPP02, ELP2000-82B or VSOP87 theories. Apache-2.0 covers only this
project's own work — its code, and its compilation and rearrangement of the
published series — and not the theories themselves. Downstream users who
need to rely on the terms of the underlying theories should consult the IMCCE
legal notice,
https://www.imcce.fr/mentions-legales
which restricts reproduction and makes commercial or professional use
subject to IMCCE authorisation.
ELP/MPP02 terms
The official ELP/MPP02 distribution ships README.TXT (December 2002) and the
explanatory note elpmpp02.pdf; neither states a licence or terms of use, and
no separate terms file is included. This project therefore treats ELP/MPP02
as carrying no stated terms and relies on the disclaimer under "Rights in
the underlying theories" above. Note also that IMCCE's ftp server carries
only ELP2000-82B, not ELP/MPP02. Downstream users who need a formal position
should consult SYRTE, https://syrte.obspm.fr/, and the IMCCE legal notice.
================================================================================
3. JPL ephemerides — DE440 / DE441 and Horizons kernels
================================================================================
Files
basic/testdata/semianalytic/planet_apparent_de441.csv
planet/internal/semianalytic/testdata/de441-states.csv
planet/spk/testdata/de441_ref.csv
planet/spk/testdata/ceres.bsp
Source
JPL DE440/DE441 planetary ephemerides via NAIF,
https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/
and JPL Horizons, https://ssd.jpl.nasa.gov/horizons/
The DE440/DE441 data are also the fitting target of the tables in section 2.
Terms
Produced by the United States Government (NASA/JPL); not subject to
copyright protection in the United States. NAIF and PDS ask that the source
be credited, as done here.
================================================================================
4. Natural Earth land polygons — internal/svgmap/land_*.path
================================================================================
Source
Natural Earth 1:50m physical land, without administrative boundaries,
https://www.naturalearthdata.com/
Terms
Public domain. Natural Earth states that the data may be used for any
purpose without permission. Credit is given here as good practice, and the
rendered charts carry the "Natural Earth 1:50m physical land" label.
================================================================================
5. Star catalogue data — basic/star_catalog.dat
================================================================================
Astrometry
The built-in catalogue carries Bright Star Catalogue / Harvard (HR) numbers
1..9110 with J2000 positions, proper motions and magnitudes. Its astrometric
content derives from the Yale Bright Star Catalogue (Hoffleit & Warren),
distributed by CDS as V/50,
https://cdsarc.cds.unistra.fr/viz-bin/cat/V/50
Catalogue data from CDS is distributed free of charge subject to the CDS
general terms of use and to citation of the source.
================================================================================
References
================================================================================
ISAS Data Policy ...... https://www.isas.jaxa.jp/en/researchers/data-policy/
CC BY 4.0 ............. https://creativecommons.org/licenses/by/4.0/
SELENE Data Archive ... https://darts.isas.jaxa.jp/
IMCCE legal notice .... https://www.imcce.fr/mentions-legales
Natural Earth ......... https://www.naturalearthdata.com/
NAIF SPK kernels ...... https://naif.jpl.nasa.gov/pub/naif/generic_kernels/
JPL Horizons .......... https://ssd.jpl.nasa.gov/horizons/
CDS VizieR V/50 ....... https://cdsarc.cds.unistra.fr/viz-bin/cat/V/50
SYRTE (Observatoire de Paris) ... https://syrte.obspm.fr/
ELP/MPP02 distribution .......... ftp://cyrano-se.obspm.fr/pub/2_lunar_solutions/2_elpmpp02/
+71 -31
View File
@@ -6,9 +6,20 @@
An astronomy library I have used for years for my own interest in astronomy and calendars.
Based on Jean Meeus's *Astronomical Algorithms*. Solar and planetary calculations use built-in VSOP87 terms; lunar calculations use ELP2000/82-style truncated series. No external ephemeris files are required.
> ### ⚠️ Time-scale change: UTC switches to leap hours on 2027-05-20
>
> On 15 October 2026 the 28th General Conference on Weights and Measures (CGPM) adopted [Resolution 5, "On the technical actions needed to ensure the continuity of UTC"](https://www.bipm.org/en/cgpm-2026/resolution-5) (`Resolution-CGPM-28-5`, DOI `10.59161/CGPM2026RES5E`): **continuous UTC becomes effective on 20 May 2027** and the maximum value of `|UT1−UTC|` is widened from 0.9 seconds to **3600 seconds (1 hour)** — a leap hour in place of a leap second.
>
> [IERS Bulletin C 72 (2026-07-06)](https://datacenter.iers.org/data/latestVersion/bulletinC.txt) confirms that **no leap second will be introduced at the end of December 2026**; the leap-second mechanism ends on 2027-05-20, so the possible insertion point at the end of June 2027 will not carry one either.
>
> From this release the library keeps **1972–2026 on the definitive leap-second table** (those historical values are unchanged) and changes the default policy beyond the built-in window (which ends 2026-10-01) from `TimeScaleLeapSecond` to **`TimeScaleLeapHour`**; past the window TT−UTC stays at 69.184 seconds. The numeric values of the `SetTimeScaleFuturePolicy` constants changed accordingly (`TimeScaleLeapHour` is now the zero value), so callers passing raw integers must update. See [Time scales](doc/manual/en/timescale.md).
Intended for calendar calculations, amateur observation and studying astronomical algorithms.
The library ships two ephemerides, selectable in one process or used side by side:
1. **Semianalytic model** (`ModelSemianalytic`, default): the argument structure and initial amplitudes of VSOP2013/TOP2013 (planets) and ELP/MPP02 (Moon), refitted to the DE ephemerides — DE441 for the planetary long range, DE440 for the modern segment and DE441 for the Moon — and folded into a single compact series table. The planetary table is about 452 KiB and the lunar table about 42 KiB, with no external ephemeris files. Its geocentric apparent direction error (RMS) in the modern range is about 0.007″ (Sun) and 0.076″ (Moon).
2. **Legacy model** (`ModelLegacy`): the VSOP87/ELP2000-82 series with added long-term and per-body corrections. Switch to it to reproduce old values; its geocentric apparent direction error (RMS) in the modern range is about 0.04″ (Sun) and 0.75″ (Moon). See [Accuracy and Performance](doc/manual/en/accuracy.md).
External ephemerides can also be supplied through SPK or a custom engine.
## Install
@@ -19,15 +30,17 @@ go get b612.me/astro
## Highlights
- Civil and Chinese calendar conversion from 721 BCE to 3000 CE, solar terms, sexagenary dates, era names and historical calendars.
- Positions, rise/set, transit, distances, angular diameters and physical ephemerides for the Sun, Moon and seven planets; lightweight Sun and Moon algorithms are also available.
- Solar and lunar eclipses, stellar and planetary lunar occultations, local contacts and global paths.
- SVG charts, GeoJSON data and KML for viewing and time playback in Google Earth.
- A built-in catalog of 9,100 stars, with constellation lookup, coordinate corrections and rise/set calculations.
- Coordinate transformations, sidereal time, precession, nutation, refraction, small-body orbits, sundials and astronomical formulas.
- Positions, rise/set, transit, distance, apparent diameter and physical ephemerides for the Sun, Moon and seven planets; lightweight Sun and Moon algorithms are also included.
- Solar and lunar eclipses, stellar and planetary lunar occultations, event searches, local contact times and global paths.
- Generic sky-event searches: ecliptic-longitude crossings, stationary points, phases, closest approach, rise/set at any altitude, perihelion/aphelion and heliacal visibility.
- Switchable ephemeris models, multiple instances used in parallel in one process, SPK multi-source stacking and optional perturbed propagation.
- SVG sky-event charts, GeoJSON geographic data and KML for viewing and time playback in Google Earth.
- A built-in catalog of 9100 stars with constellation lookup, coordinate corrections and rise/set calculations; CSV, Hipparcos, BSC5 and Stellarium external catalogs are also available.
- Celestial coordinate transformations, sidereal time, precession and nutation, refraction, astronomical formulas, sundial geometry and an optional lunar-limb terrain correction.
## Example
Calculate sunrise in Xi'an, the lunar phase and the Chinese calendar date:
Calculate the sunrise time, lunar phase and Chinese calendar date for a day in Xi'an:
```go
package main
@@ -61,49 +74,76 @@ func main() {
}
```
Longitude and latitude are in degrees, positive east and north. The example uses a height of 0 metres and enables refraction with `true`. Rise/set functions return errors for polar conditions or a missing event on that date.
Longitude and latitude are in degrees, positive east and north; the example uses a height of 0 metres and `true` makes the rise/set calculation account for atmospheric refraction. Rise/set functions return an error during polar day, polar night or when no event occurs on that date.
Calendar and phase descriptions in this example are returned in Chinese.
Package-level functions use the process-wide ephemeris model (semianalytic in default builds). To pin a model for a calculation, use `astro.NewContext`; multiple contexts can be used in parallel in one process:
```go
func compareModels(date time.Time) {
legacy, err := astro.NewContext(astro.ModelLegacy)
if err != nil {
panic(err)
}
modern, err := astro.NewContext(astro.ModelSemianalytic)
if err != nil {
panic(err)
}
fmt.Println(legacy.Moon().PhaseDesc(date), modern.Moon().PhaseDesc(date))
riseLegacy, _ := legacy.Sun().RiseTime(date, 108.93, 34.27, 0, true)
riseModern, _ := modern.Sun().RiseTime(date, 108.93, 34.27, 0, true)
fmt.Println(riseLegacy.Format("15:04:05"), riseModern.Format("15:04:05"))
}
```
A context exposes per-module entry points such as `Sun()`, `Moon()`, `Mars()`, `Eclipse()` and `Phenomena()`, with the same signatures as their package-level counterparts. Models isolate the ephemeris only; ΔT, TT−UTC and civil time-scale policy remain process-wide settings. See [Explicit ephemeris instances](doc/manual/en/ephemeris.md) for custom engines, SPK and multi-source stacking.
## Package Overview
| Package | Purpose and documentation |
| --- | --- |
| `calendar` | [Calendar conversion, solar terms, sexagenary dates and historical calendars](doc/manual/en/calendar.md) |
| `sun` / `moon` / `lite/sun` / `lite/moon` | [Sun and Moon positions, rise/set, phases and physical quantities](doc/manual/en/sun-moon.md) |
| `mercury` / `venus` / `mars` / `jupiter` / `saturn` / `uranus` / `neptune` / `earth` | [Planetary positions, events, Jupiter's satellites and Saturn's rings](doc/manual/en/planets.md) |
| `eclipse` / `eclipse/svg` | [Eclipse searches, local visibility, paths and charts](doc/manual/en/eclipse.md) |
| `moon` / `moon/svg` | [Stellar and planetary lunar occultations, contacts and bands](doc/manual/en/occultation.md) |
| `sun` / `moon` / `lite/sun` / `lite/moon` | [Sun and Moon positions, rise/set, lunar phases and physical quantities](doc/manual/en/sun-moon.md) |
| `mercury` / `venus` / `mars` / `jupiter` / `saturn` / `uranus` / `neptune` / `earth` | [Planetary positions, special events, Jupiter's satellites and Saturn's rings](doc/manual/en/planets.md) |
| `eclipse` / `eclipse/svg` | [Solar and lunar eclipse and transit searches, local visibility, paths and charts](doc/manual/en/eclipse.md) |
| `moon` / `moon/svg` / `moon/limb` | [Stellar and planetary lunar occultations, contact times, bands and the optional lunar-limb terrain correction](doc/manual/en/occultation.md) |
| `phenomena` | [Ecliptic-longitude crossings, stationary points, phases, closest approach, heliacal visibility and rise/set at any altitude for any two bodies](doc/manual/en/phenomena.md) |
| `geojson` / `kml` | [Event maps, GeoJSON and KML](doc/manual/en/map-geojson.md) |
| `star` | [Star catalog, constellations and observing quantities](doc/manual/en/star.md) |
| `coord` | [Coordinates, sidereal time, precession, nutation and refraction](doc/manual/en/coord.md) |
| `orbit` | [Asteroid and comet two-body orbits, visual binaries](doc/manual/en/orbit.md) |
| `star` / `star/catalog` | [Star database, constellations, observing quantities and external catalogs](doc/manual/en/star.md#external-catalogs) |
| `coord` | [Coordinate conversion, sidereal time, precession, nutation and refraction](doc/manual/en/coord.md) |
| `orbit` | [Asteroid and comet two-body or optional perturbed propagation, visual binaries](doc/manual/en/orbit.md) |
| `sundial` | [Apparent solar time and sundial geometry](doc/manual/en/sundial.md) |
| `formula` | [Radiation, magnitudes, synodic periods and telescope formulas](doc/manual/en/formula.md) |
| `astro` (root) | [UTC, UT1, TT and ΔT models](doc/manual/en/timescale.md) |
| `formula` | [Radiation, magnitudes, telescopes, atmospheric refraction, extinction and Earth-orientation tools](doc/manual/en/formula.md) |
| `astro` / `basic` / `planet/spk` | [Ephemeris models and instances, multi-source ephemerides and custom engines](doc/manual/en/ephemeris.md) |
| `astro` (root) | [UTC, UT1, TT conversion and ΔT models](doc/manual/en/timescale.md) |
`basic` contains the underlying algorithms, `planet` holds analytical series, and `tools` provides numerical helpers. Most applications can use the packages above.
Full signatures are also available in the [Go API documentation](https://pkg.go.dev/b612.me/astro).
`basic` contains the underlying algorithms, `planet` holds the built-in models and series, `planet/spk` reads external kernels, and `tools` provides numerical helpers. See [Accuracy and performance](doc/manual/en/accuracy.md) for model composition, measured errors and performance; full signatures are also available in the [Go API documentation](https://pkg.go.dev/b612.me/astro).
## Time Scale Conventions
Most observing APIs accept a civil instant as `time.Time` and handle UTC, UT1 and TT conversions internally. Before 1972 the library treats civil time as UT1, and as UTC thereafter. Orbital epochs and some low-level APIs have separate scale requirements.
Most observing APIs accept a civil instant as `time.Time` and handle UTC, UT1 and TT conversions internally. Before 1972 the library treats civil time as UT1, and as UTC thereafter; orbital epochs and some low-level APIs have separate time-scale requirements.
Angles default to degrees, angular diameters to arcseconds, sidereal time to hours, and distances to AU or km as specified by the API. SVG and GeoJSON time labels default to UTC, with explicit UT1 options.
Angles default to degrees, apparent diameters to arcseconds, sidereal time to hours, and distances to AU or km as specified by the API. SVG and GeoJSON time labels default to UTC, with an explicit UT1 option. See [Time scales](doc/manual/en/timescale.md) for the exact conventions.
See [Time scales](doc/manual/en/timescale.md).
DUT1 and TT−UTC default to extrapolating the current leap-second rule beyond the observed window (through September 2026); `SetTimeScaleFuturePolicy` switches the TT−UTC convention to one of four alternatives: a frozen offset, continued UT1 tracking, the leap-hour rule, or a civil scale permanently identical to UT1.
Beyond the monthly ΔT table, DUT1 and TT−UTC default to **leap-hour** extrapolation (`TimeScaleLeapHour`, matching the continuous UTC effective 2027-05-20); `SetTimeScaleFuturePolicy` switches between `TimeScaleLeapHour` (default), `TimeScaleLeapSecond`, `TimeScaleAssumeUT1Tracking`, `TimeScaleFreezeUTCOffset` and `TimeScaleUT1Civil`.
## Observer Height Convention
Observer height is ellipsoidal height in metres. Convert orthometric height `H` using the local geoid undulation `N`: `height = H + N`. See [Observer height](doc/manual/en/coord.md#observer-height).
Observer height is ellipsoidal height in metres. If only orthometric height `H` is available, convert it with the local geoid undulation `N`: `height = H + N`. See [Observer height](doc/manual/en/coord.md#observer-height).
## Scope And Accuracy
The library uses analytical models and truncated series. Accuracy varies with date, body and calculation. Professional occultation predictions and spacecraft navigation require more precise ephemerides and physical models.
Accuracy varies with model, epoch, body and calculated quantity. The default model has changed from VSOP87/ELP2000-82 to the semianalytic tables: signatures are unchanged but values may differ. `lite/sun` and `lite/moon` use independent approximation chains and do not follow model switches.
`lite/sun` and `lite/moon` suit resource-constrained applications. Some main APIs also provide truncated variants with an `N` suffix.
See [Accuracy and performance](doc/manual/en/accuracy.md) for model composition, error conventions and measured results. Professional occultation prediction, spacecraft navigation and similar uses also need ephemerides, time scales and physical models of matching accuracy; loading an SPK alone does not make every high-level result as accurate as the kernel.
Model ranges, measured differences and benchmarks are documented under [Accuracy and performance](doc/manual/en/accuracy.md).
## License
The code and documentation are licensed under [Apache-2.0](LICENSE). The third-party data embedded in this repository is **not** covered by Apache-2.0 and follows the terms of its own source; see [NOTICE](NOTICE) for the item-by-item statement:
- **SELENE (KAGUYA) LALT lunar limb profile** (`moon/limb/kaguya_lalt.bin`): ISAS/JAXA, distributed under the ISAS Data Policy, which states that its rules are compatible with CC BY 4.0.
- **This project's own refits of VSOP2013/TOP2013 and ELP/MPP02** (`planet/internal/semianalytic/`): the underlying theories are IMCCE / SYRTE. This project can neither grant nor restrict rights in those theories; Apache-2.0 applies only to this project's own code and compilation.
- **JPL DE440/DE441 and Horizons data** (`*/testdata/`, `planet/spk/testdata/`): NASA/JPL.
- **Astrometry in the built-in star catalogue** (`basic/star_catalog.dat`): derived from the Yale Bright Star Catalogue (VizieR [V/50](https://cdsarc.cds.unistra.fr/viz-bin/cat/V/50)).
- **Natural Earth 1:50m physical land** (`internal/svgmap/land_*.path`): public domain.
+63 -13
View File
@@ -6,7 +6,20 @@
自用多年的天文算法库,用于个人天文历法爱好。
基于 Jean Meeus《天文算法》(Astronomical Algorithms)实现。太阳和行星使用内置 VSOP87 解析项,月球使用 ELP2000/82 风格截断级数,不依赖外部星历文件。适合历法计算、业余观测和天文算法学习。
> ### ⚠️ 时标变更:2027-05-20 起 UTC 改用闰时
>
> 2026 年 10 月 15 日,第 28 届国际计量大会(CGPM)通过[第 5 号决议《关于确保 UTC 连续性所需的技术行动》](https://www.bipm.org/en/cgpm-2026/resolution-5)(`Resolution-CGPM-28-5`,DOI `10.59161/CGPM2026RES5E`):**连续 UTC 自 2027 年 5 月 20 日起生效**,`|UT1−UTC|` 的最大值由 0.9 秒放宽到 **3600 秒(1 小时)**,即以闰时取代闰秒。
>
> [IERS Bulletin C 72(2026-07-06)](https://datacenter.iers.org/data/latestVersion/bulletinC.txt) 已确认 **2026 年 12 月底不插入闰秒**;闰秒机制在 2027-05-20 终止,2027 年 6 月底这一可能的插入点同样不会有闰秒。
>
> 本库从本版本起:**1972–2026 仍按确定的闰秒表计算**,历史值不变;内置窗口(截至 2026-10-01)之后的默认政策由 `TimeScaleLeapSecond` 改为 **`TimeScaleLeapHour`**,窗口之后 TT−UTC 将长期保持 69.184 秒。`SetTimeScaleFuturePolicy` 的常量数值随之重排(`TimeScaleLeapHour` 现为零值),传裸整数的调用方需要更新。详见[时标](doc/manual/timescale.md)。
本库内置两套星历,可在同一进程内切换或并行使用:
1. **半解析模型**(`ModelSemianalytic`,默认):以 VSOP2013/TOP2013(行星)和 ELP/MPP02(月球)为基准,按 DE 星历重新拟合——行星长时段用 DE441、现代段用 DE440,月球用 DE441——折叠成一张紧凑级数表。行星表约 452 KiB、月球表约 42 KiB,无需外部星历文件。现代段地心视方向均方根误差约 0.007″(太阳)、0.076″(月球)。
2. **旧模型**(`ModelLegacy`):VSOP87/ELP2000-82 级数,另加长期项与逐天体订正。需要复现旧版数值时切换;现代段地心视方向均方根误差约 0.04″(太阳)、0.75″(月球),详见[精度与性能](doc/manual/accuracy.md)。
除内置模型外,也可以通过 SPK 或自定义引擎接入外部星历。
## 安装
@@ -19,9 +32,11 @@ go get b612.me/astro
- 公历与农历互转,支持公元前 721 年至公元 3000 年;节气、干支、年号与古历。
- 太阳、月亮、七大行星的位置、升落、中天、距离、视直径与物理星历;另有轻量日月算法。
- 日月食、恒星与行星月掩的事件搜索、地方接触时刻和全球路径。
- 通用天象搜索:黄经穿越、留、相位、最近角距、任意高度升落、近远日点与偕日可见。
- 可切换星历模型、同一进程并行使用多个实例,支持 SPK 多源叠加与可选摄动传播。
- SVG 天象图、GeoJSON 地理数据和可供 Google Earth 查看及按时间播放的 KML。
- 内置 9100 颗恒星,支持星座判定、坐标修正与升落计算。
- 天球坐标转换、恒星时、岁差章动、折射、通用小天体轨道、日晷和常用天文公式。
- 内置 9100 颗恒星,支持星座判定、坐标修正与升落计算;另有 CSV、Hipparcos、BSC5 与 Stellarium 外挂星表。
- 天球坐标转换、恒星时、岁差章动、折射、天文公式、日晷几何与可选月缘地形修正。
## 使用示例
@@ -61,6 +76,29 @@ func main() {
经纬度以东经、北纬为正,单位为度;示例高度为 0 米,`true` 表示升落计算考虑大气折射。极昼、极夜或当天没有升落事件时,升落接口会返回错误。
包级函数使用进程当前的星历模型(默认构建为半解析)。要给某次计算固定模型,用 `astro.NewContext`,同一进程里可以有多个上下文并行:
```go
func compareModels(date time.Time) {
legacy, err := astro.NewContext(astro.ModelLegacy)
if err != nil {
panic(err)
}
modern, err := astro.NewContext(astro.ModelSemianalytic)
if err != nil {
panic(err)
}
fmt.Println(legacy.Moon().PhaseDesc(date), modern.Moon().PhaseDesc(date))
riseLegacy, _ := legacy.Sun().RiseTime(date, 108.93, 34.27, 0, true)
riseModern, _ := modern.Sun().RiseTime(date, 108.93, 34.27, 0, true)
fmt.Println(riseLegacy.Format("15:04:05"), riseModern.Format("15:04:05"))
}
```
上下文按模块提供 `Sun()`、`Moon()`、`Mars()`、`Eclipse()`、`Phenomena()` 等入口,方法与同名包级函数签名一致。模型只隔离星历,ΔT、TT−UTC 和民用时标政策仍是进程级设置。自定义引擎、SPK 与多源叠加见[显式星历实例](doc/manual/ephemeris.md)。
## 包概览
| 包 | 用途与文档 |
@@ -68,25 +106,27 @@ func main() {
| `calendar` | [历法转换、节气、干支与古历](doc/manual/calendar.md) |
| `sun` / `moon` / `lite/sun` / `lite/moon` | [太阳与月亮的位置、升落、月相和物理量](doc/manual/sun-moon.md) |
| `mercury` / `venus` / `mars` / `jupiter` / `saturn` / `uranus` / `neptune` / `earth` | [行星位置、特殊天象、木星卫星与土星环](doc/manual/planets.md) |
| `eclipse` / `eclipse/svg` | [日月食查询、地方可见性、路径与出图](doc/manual/eclipse.md) |
| `moon` / `moon/svg` | [恒星及行星月掩、接触时刻与掩带](doc/manual/occultation.md) |
| `eclipse` / `eclipse/svg` | [日月食与凌日的查询、地方可见性、路径与出图](doc/manual/eclipse.md) |
| `moon` / `moon/svg` / `moon/limb` | [恒星及行星月掩、接触时刻、掩带与可选月缘地形修正](doc/manual/occultation.md) |
| `phenomena` | [任意两天体黄经穿越、留、相位、最近角距、偕日可见与任意高度升落](doc/manual/phenomena.md) |
| `geojson` / `kml` | [天象地图、GeoJSON 与 KML](doc/manual/map-geojson.md) |
| `star` | [恒星数据库、星座与观测量](doc/manual/star.md) |
| `star` / `star/catalog` | [恒星数据库、星座、观测量与外挂星表](doc/manual/star.md#外挂星表) |
| `coord` | [坐标转换、恒星时、岁差、章动与折射](doc/manual/coord.md) |
| `orbit` | [小行星、彗星等二体轨道与视双星](doc/manual/orbit.md) |
| `orbit` | [小行星、彗星的二体或可选摄动传播与视双星](doc/manual/orbit.md) |
| `sundial` | [真太阳时与日晷几何](doc/manual/sundial.md) |
| `formula` | [辐射、星等、会合周期与望远镜公式](doc/manual/formula.md) |
| `formula` | [辐射、星等、望远镜、大气折射、消光与地球姿态工具](doc/manual/formula.md) |
| `astro` / `basic` / `planet/spk` | [星历模型与实例、多源星历与自定义引擎](doc/manual/ephemeris.md) |
| `astro`(根包) | [UTC、UT1、TT 换算与 ΔT 模型](doc/manual/timescale.md) |
`basic` 是底层算法包,`planet` 保存解析级数,`tools` 提供数值辅助函数。一般使用上表中的包即可;完整函数签名也可查 [Go API 文档](https://pkg.go.dev/b612.me/astro)。
`basic` 是底层算法包,`planet` 保存内置模型与级数,`planet/spk` 读取外部核,`tools` 提供数值辅助函数。模型构成、实测误差和性能见[精度与性能](doc/manual/accuracy.md);完整函数签名也可查 [Go API 文档](https://pkg.go.dev/b612.me/astro)。
## 时标约定
一般观测接口接收表示民用时刻的 `time.Time`,内部处理 UTC、UT1 与 TT 换算。1972 年以前,库将民用时间按 UT1 处理,之后按UTC处理;轨道历元和部分底层接口另有时标要求。
一般观测接口接收表示民用时刻的 `time.Time`,内部处理 UTC、UT1 与 TT 换算。1972 年以前,库将民用时间按 UT1 处理,之后按 UTC 处理;轨道历元和部分底层接口另有时标要求。
角度默认用度,视直径用角秒,恒星时用小时,距离按接口使用 AU 或 km。SVG 和 GeoJSON 默认输出 UTC 时间标签,也可显式选择 UT1。具体约定见[时标手册](doc/manual/timescale.md)。
DUT1 与 TT−UTC 在实测窗口(截至 2026 年 9 月)之后默认沿用现行闰秒规则外推,可用 `SetTimeScaleFuturePolicy` 调整 TT−UTC 口径到"冻结偏移量"、"继续跟随 UT1"、"闰时规则"、"持续等价UT1"四个规则。
DUT1 与 TT−UTC 在月度 ΔT 表覆盖范围之后默认按**闰时**外推(`TimeScaleLeapHour`,对应 2027-05-20 起的连续 UTC),可用 `SetTimeScaleFuturePolicy` 在 `TimeScaleLeapHour`(默认)、`TimeScaleLeapSecond`、`TimeScaleAssumeUT1Tracking`、`TimeScaleFreezeUTCOffset`、`TimeScaleUT1Civil` 之间切换。
## 观测点高度约定
@@ -94,6 +134,16 @@ DUT1 与 TT−UTC 在实测窗口(截至 2026 年 9 月)之后默认沿用
## 适用范围与精度
本库使用解析模型和截断级数。精度随年代、天体与计算项目变化;专业掩星、航天导航等用途需要更精确的星历和物理模型。
精度随模型、年代、天体和计算项目变化。默认模型已从 VSOP87/ELP2000-82 换成半解析表,签名不变但数值可能不同;`lite/sun`、`lite/moon` 使用独立近似链路,不随模型切换。
`lite/sun`、`lite/moon` 适合资源受限环境;部分主接口也提供带 `N` 后缀的截断版本。模型范围、误差样本和性能数据见[精度与性能](doc/manual/accuracy.md)。
模型构成、误差口径和实测结果见[精度与性能](doc/manual/accuracy.md)。专业掩星、航天导航等用途还需要相应精度的星历、时标与物理模型,接入 SPK 本身不保证全部高层结果达到核文件精度。
## 许可
代码与文档采用 [Apache-2.0](LICENSE)。仓库内嵌的第三方数据**不适用** Apache-2.0,各自遵循其来源条款,逐项声明见 [NOTICE](NOTICE):
- **SELENE(KAGUYA)LALT 月缘剖面**(`moon/limb/kaguya_lalt.bin`):ISAS/JAXA,按 ISAS 数据政策分发(该政策声明其规则与 CC BY 4.0 兼容)。
- **VSOP2013/TOP2013 与 ELP/MPP02 的自行再拟合表**(`planet/internal/semianalytic/`):上游理论属 IMCCE / SYRTE。 Apache-2.0 只适用本库自己的代码与汇编。
- **JPL DE440/DE441 与 Horizons 数据**(`*/testdata/`、`planet/spk/testdata/`):NASA/JPL。
- **内置星表的天测数据**(`basic/star_catalog.dat`):源自 Yale Bright Star Catalogue(VizieR [V/50](https://cdsarc.cds.unistra.fr/viz-bin/cat/V/50))。
- **Natural Earth 1:50m 物理陆地**(`internal/svgmap/land_*.path`):公有领域。
+6 -6
View File
@@ -1,4 +1,4 @@
// Package astro 提供进程级时标配置与时标换算 / process-wide time-scale configuration and conversions.
// Package astro 提供高层星历上下文、模型切换与进程级时标配置 / high-level ephemeris contexts, model switching and process-wide time-scale configuration.
package astro
import "b612.me/astro/basic"
@@ -33,18 +33,18 @@ func DefaultTTMinusUTC() func(float64) float64 {
return basic.TTMinusUTCSecondsDefault
}
// TimeScaleFuturePolicy 民用时标换算政策,与输出用的 TimeScale 无关 / civil-time conversion policy, separate from output TimeScale.
// TimeScaleFuturePolicy 民用时标换算政策,零值为闰时,与输出用的 TimeScale 无关 / civil-time conversion policy, leap hour as the zero value; separate from output TimeScale.
type TimeScaleFuturePolicy = basic.TimeScaleFuturePolicy
const (
// TimeScaleLeapSecond 默认按 ΔT 越限施加整数秒校正,不代表闰秒公告 / default integer-second corrections driven by extrapolated ΔT, not announcements.
// TimeScaleLeapHour 按 ΔT 越限施加整小时校正(2027 起 CGPM 决议) / applies hour-sized corrections as adopted by CGPM from 2027.
TimeScaleLeapHour = basic.TimeScaleLeapHour
// TimeScaleLeapSecond 按 ΔT 越限施加整数秒校正(1972–2026 确定值) / integer-second corrections (definitive 1972–2026).
TimeScaleLeapSecond = basic.TimeScaleLeapSecond
// TimeScaleAssumeUT1Tracking 窗口外固定末端 DUT1,平滑跟随 UT1 / holds the last observed DUT1 beyond the window.
TimeScaleAssumeUT1Tracking = basic.TimeScaleAssumeUT1Tracking
// TimeScaleFreezeUTCOffset 假设 UTC 偏移冻结在精确窗口末端 / freezes the UTC offset at the window end.
// TimeScaleFreezeUTCOffset 窗口外固定末端 TT−UTC / holds the last TT−UTC offset beyond the window.
TimeScaleFreezeUTCOffset = basic.TimeScaleFreezeUTCOffset
// TimeScaleLeapHour 按 ΔT 越限施加整小时校正,仅作情景演算 / applies hour-sized corrections as a scenario assumption.
TimeScaleLeapHour = basic.TimeScaleLeapHour
// TimeScaleUT1Civil 全时轴民用时标等同 UT1,TT−UTC 覆盖仍优先 / uses UT1 as civil time everywhere unless TT−UTC is overridden.
TimeScaleUT1Civil = basic.TimeScaleUT1Civil
)
+5
View File
@@ -13,6 +13,8 @@ import (
"b612.me/astro/moon"
"b612.me/astro/planet"
"b612.me/astro/sun"
"b612.me/astro/internal/ephemtest"
)
type baselinePlanetSnapshot struct {
@@ -171,6 +173,7 @@ func assertTruncationConverges(t *testing.T, cases []truncationCase, samples int
}
func TestPlanetMoonBaselineRegression(t *testing.T) {
ephemtest.PinLegacy(t)
samples := loadBaselineSamples(t)
for _, sample := range samples {
for _, body := range sample.Planets {
@@ -239,6 +242,7 @@ func planetMoonTruncationCases(samples []baselineSample) []truncationCase {
}
func TestPlanetMoonTruncationConvergesToFullSeries(t *testing.T) {
ephemtest.PinLegacy(t)
samples := loadBaselineSamples(t)
assertTruncationConverges(t, planetMoonTruncationCases(samples), len(samples))
}
@@ -257,6 +261,7 @@ func publicSunMoonTruncationCases(t *testing.T, samples []baselineSample) []trun
}
func TestPublicSunMoonTruncationConvergesToFullSeries(t *testing.T) {
ephemtest.PinLegacy(t)
samples := loadBaselineSamples(t)
assertTruncationConverges(t, publicSunMoonTruncationCases(t, samples), len(samples))
}
+1794
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
// 被测契约:日月视赤经纬单值与双值接口一致,且黄经、黄纬取自同一视方向。
package basic
import (
"math"
"testing"
"b612.me/astro/internal/ephem"
"b612.me/astro/internal/ephemtest"
)
func TestSunMoonApparentRaDecUseFullDirection(t *testing.T) {
ephemtest.Pin(t, ephem.Semianalytic)
const jde = 2460409.25
for _, body := range []ephem.Body{ephem.Sun, ephem.Moon} {
lo, bo, _, _, ok := vectorApparent(body, jde, -1)
if !ok {
t.Fatalf("body %d: no vector apparent position", body)
}
wantRA, wantDec := LoBoToRaDec(jde, lo, bo)
var ra, dec float64
if body == ephem.Sun {
ra, dec = HSunApparentRaDec(jde)
if r, d := HSunApparentRa(jde), HSunApparentDec(jde); r != ra || d != dec {
t.Errorf("Sun single (%v, %v) != pair (%v, %v)", r, d, ra, dec)
}
} else {
ra, dec = HMoonGeocentricApparentRaDecN(jde, -1)
}
if math.Abs(ra-wantRA)*3600 > 1e-6 || math.Abs(dec-wantDec)*3600 > 1e-6 {
t.Errorf("body %d: (%v, %v), want (%v, %v)", body, ra, dec, wantRA, wantDec)
}
}
}
+46
View File
@@ -0,0 +1,46 @@
// 被测契约:包级与实例视位置共用同一判据,零方向、非正距离、负光行时与非有限值一律报错。
package basic
import (
"math"
"testing"
"b612.me/astro/internal/ephem"
)
type fixedApparentEngine struct {
dir [3]float64
dist, tau float64
}
func (e fixedApparentEngine) State(target, center ephem.Body, jdTDB float64) (pos, vel [3]float64, err error) {
return pos, vel, ephem.ErrUnsupportedBody
}
func (e fixedApparentEngine) Coverage(ephem.Body) (start, end float64, ok bool) { return 0, 0, false }
func (e fixedApparentEngine) Apparent(ephem.Body, float64) ([3]float64, float64, float64, error) {
return e.dir, e.dist, e.tau, nil
}
func TestApparentStateRejectsInvalid(t *testing.T) {
nan := math.NaN()
for _, c := range []struct {
name string
e fixedApparentEngine
ok bool
}{
{"valid", fixedApparentEngine{[3]float64{1, 0, 0}, 1.5e8, 0.0058}, true},
{"nan direction", fixedApparentEngine{[3]float64{nan, 0, 0}, 1.5e8, 0.0058}, false},
{"zero direction", fixedApparentEngine{[3]float64{}, 1.5e8, 0.0058}, false},
{"zero distance", fixedApparentEngine{[3]float64{1, 0, 0}, 0, 0.0058}, false},
{"inf distance", fixedApparentEngine{[3]float64{1, 0, 0}, math.Inf(1), 0.0058}, false},
{"negative tau", fixedApparentEngine{[3]float64{1, 0, 0}, 1.5e8, -1}, false},
{"nan tau", fixedApparentEngine{[3]float64{1, 0, 0}, 1.5e8, nan}, false},
} {
_, _, _, _, ok, err := apparentState(ephem.Sun, 2451545, ephem.NewInstance(c.e))
if c.ok && (err != nil || !ok) || !c.ok && err == nil {
t.Errorf("%s: ok=%v err=%v", c.name, ok, err)
}
}
}
+77
View File
@@ -0,0 +1,77 @@
package basic
import (
"math"
"b612.me/astro/internal/ephem"
)
// planetBody 旧模型行星序号 1..7(水星..海王星,不含地球)对应的 NAIF 编号。
var planetBody = [8]ephem.Body{0, ephem.Mercury, ephem.Venus, ephem.Mars, ephem.Jupiter, ephem.Saturn, ephem.Uranus, ephem.Neptune}
// vectorApparent 新模型全精度时的矢量视位置:真黄道经纬(度)、光行时(日)、几何地心距(AU);不适用时 ok=false。
// 状态非法时与显式实例路径一致:ok=true 且各值为 NaN,不得退回低精度公式掩盖错误。
func vectorApparent(target ephem.Body, jde float64, n int) (lo, bo, tau, distAU float64, ok bool) {
if n >= 0 || ephem.Active() != ephem.Semianalytic {
return 0, 0, 0, 0, false
}
lo, bo, tau, distAU, ok, err := apparentState(target, jde, ephem.Global())
if err != nil {
return math.NaN(), math.NaN(), math.NaN(), math.NaN(), true
}
return lo, bo, tau, distAU, ok
}
// icrfToTrueEcliptic ICRF 单位矢量 → 真黄道经纬(度);黄赤交角与下游 LoBoToRaDec 同口径,往返得到真赤道。
func icrfToTrueEcliptic(d [3]float64, jde float64) (lo, bo float64) {
m := ltpPBMAT(2000 + (jde-2451545)/365.25)
var q [3]float64
for i := range q {
q[i] = m[i][0]*d[0] + m[i][1]*d[1] + m[i][2]*d[2]
}
s, c := math.Sincos(Obliquity1980(jde) * math.Pi / 180)
x, y, z := q[0], q[1]*c+q[2]*s, -q[1]*s+q[2]*c
lo = math.Atan2(y, x)*180/math.Pi + Nutation2000Bi(jde)
bo = math.Atan2(z, math.Hypot(x, y)) * 180 / math.Pi
return math.Mod(lo+720, 360), bo
}
// sunApparentLoBoN 太阳视黄经与黄纬;有矢量视位置时两者取自同一方向,否则退回视黄经配几何黄纬。
func sunApparentLoBoN(jde float64, n int, inst *Instance) (float64, float64) {
if lo, bo, _, _, ok, err := vectorApparentWithInstance(ephem.Sun, jde, n, inst); err != nil {
return math.NaN(), math.NaN()
} else if ok {
return lo, bo
}
return HSunApparentLoNWithInstance(jde, n, inst), HSunTrueBoNWithInstance(jde, n, inst)
}
// moonApparentLoBoN 月球视黄经与黄纬;有矢量视位置时两者取自同一方向,否则退回视黄经配几何黄纬。
func moonApparentLoBoN(jde float64, n int, inst *Instance) (float64, float64) {
if lo, bo, _, _, ok, err := vectorApparentWithInstance(ephem.Moon, jde, n, inst); err != nil {
return math.NaN(), math.NaN()
} else if ok {
return lo, bo
}
return HMoonApparentLoNWithInstance(jde, n, inst), HMoonTrueBoNWithInstance(jde, n, inst)
}
func sunApparentRaDecN(jde float64, n int, inst *Instance) (float64, float64) {
lo, bo := sunApparentLoBoN(jde, n, inst)
return LoBoToRaDec(jde, lo, bo)
}
func sunApparentRaN(jde float64, n int, inst *Instance) float64 {
ra, _ := sunApparentRaDecN(jde, n, inst)
return ra
}
func moonApparentRaDecN(jde float64, n int, inst *Instance) (float64, float64) {
lo, bo := moonApparentLoBoN(jde, n, inst)
return LoBoToRaDec(jde, lo, bo)
}
func moonApparentRaN(jde float64, n int, inst *Instance) float64 {
ra, _ := moonApparentRaDecN(jde, n, inst)
return ra
}
+10 -8
View File
@@ -57,14 +57,14 @@ func EarthAphelion(year int) ApsisEvent {
return earthApsis(year, true)
}
// MoonPerigees 指定年月内的所有月球近地点 / all lunar perigees in the given Gregorian month.
func MoonPerigees(year int, month time.Month) []ApsisEvent {
return moonApsisInMonth(year, month, false)
// MoonPerigeesWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonPerigeesWithInstance(year int, month time.Month, inst *Instance) []ApsisEvent {
return moonApsisInMonth(year, month, false, inst)
}
// MoonApogees 指定年月内的所有月球远地点 / all lunar apogees in the given Gregorian month.
func MoonApogees(year int, month time.Month) []ApsisEvent {
return moonApsisInMonth(year, month, true)
// MoonApogeesWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonApogeesWithInstance(year int, month time.Month, inst *Instance) []ApsisEvent {
return moonApsisInMonth(year, month, true, inst)
}
func earthApsis(year int, aphelion bool) ApsisEvent {
@@ -84,7 +84,9 @@ func earthApsis(year int, aphelion bool) ApsisEvent {
}
}
func moonApsisInMonth(year int, month time.Month, apogee bool) []ApsisEvent {
func moonApsisInMonth(year int, month time.Month, apogee bool, instances ...*Instance) []ApsisEvent {
inst := instanceValue(instances)
startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
endUTC := startUTC.AddDate(0, 1, 0)
startTT := UTC2TT(Date2JD(startUTC))
@@ -109,7 +111,7 @@ func moonApsisInMonth(year int, month time.Month, apogee bool) []ApsisEvent {
events := make([]ApsisEvent, 0, 2)
for k := kStart; k <= kEnd; k++ {
seedTT := moonApsisSeedTT(float64(k) + phase)
eventTT, distanceKM := refineDistanceExtremum(seedTT, cfg, HMoonAway)
eventTT, distanceKM := refineDistanceExtremum(seedTT, cfg, func(jde float64) float64 { return HMoonAwayWithInstance(jde, inst) })
eventUT := TT2UTC(eventTT)
eventTimeUTC := JD2DateByZone(eventUT, time.UTC, false)
if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) {
-1
View File
@@ -87,7 +87,6 @@ func TestEarthApsisMatchesHorizonsBaseline(t *testing.T) {
}
func TestMoonApsisMatchesHorizonsBaseline(t *testing.T) {
// Baseline is generated from JPL Horizons by scripts/generate_moon_apsis_baseline.sh.
data, err := os.ReadFile("testdata/moon_apsis_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
+2 -1
View File
@@ -61,8 +61,9 @@ func TestGetJQTime(t *testing.T) {
for _, tc := range testCases {
originalResult := originalFunc(tc.year, tc.angle)
optimizedResult := GetJQTime(tc.year, tc.angle)
// 两种解法各自收敛到 1e-5 日,末步残差可达数十微秒;1e-8 日足以区分选错相邻年份的根。
diff := math.Abs(originalResult - optimizedResult)
if diff > 1e-10 {
if diff > 1e-8 {
t.Fatalf("GetJQTime mismatch: year=%d angle=%d original=%.15f optimized=%.15f diff=%.15f",
tc.year, tc.angle, originalResult, optimizedResult, diff)
}
+53 -14
View File
@@ -1,6 +1,8 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
@@ -148,29 +150,66 @@ func isCross(a, b, c, d constellationPoint) bool {
}
func resolveConstellationCode(ra, dec, jde float64) string {
var nra, ndec float64
initConstellationData()
nra = ra
if ra >= 360 {
nra -= 360
}
nra, ndec = Precess(nra, dec, jde, 2451545.0)
if ra >= 360 && nra < 270 {
nra += 360
}
if code := matchConstellationCode(nra, ndec); code != "" {
code, nra, ndec := lookupConstellationCode(ra, dec, jde)
if code != "" {
return code
}
if nra <= 270 {
ra = ra + 360
return resolveConstellationCode(ra, dec, jde)
// 只从 [0,360) 补一圈重试一次,匹配 RA 已加 360 的跨零点星座。
if nra <= 270 && ra < 360 {
if code, _, _ = lookupConstellationCode(ra+360, dec, jde); code != "" {
return code
}
}
if ndec > 50 {
return "UMI"
} else if ndec < -50 {
return "OCT"
}
return ""
if ra >= 360 {
return ""
}
// 边界数据相邻多边形之间有细缝,落在缝里的点归最近的边。
return nearestConstellationCode(nra, ndec)
}
func lookupConstellationCode(ra, dec, jde float64) (string, float64, float64) {
nra := ra
if ra >= 360 {
nra -= 360
}
nra, ndec := Precess(nra, dec, jde, 2451545.0)
if ra >= 360 && nra < 270 {
nra += 360
}
return matchConstellationCode(nra, ndec), nra, ndec
}
func nearestConstellationCode(ra, dec float64) string {
cosDec := math.Cos(dec * math.Pi / 180)
best, bestDistance := "", math.Inf(1)
for _, boundary := range constellationBoundaries {
pointCount := len(boundary.points)
for index := 0; index < pointCount; index++ {
p, q := boundary.points[index], boundary.points[(index+1)%pointCount]
px, py := math.Remainder(p.RA-ra, 360)*cosDec, p.DEC-dec
qx, qy := math.Remainder(q.RA-ra, 360)*cosDec, q.DEC-dec
if distance := originSegmentDistance(px, py, qx, qy); distance < bestDistance {
best, bestDistance = boundary.code, distance
}
}
}
return best
}
func originSegmentDistance(px, py, qx, qy float64) float64 {
dx, dy := qx-px, qy-py
length := dx*dx + dy*dy
t := 0.0
if length > 0 {
t = math.Max(0, math.Min(1, -(px*dx+py*dy)/length))
}
return math.Hypot(px+t*dx, py+t*dy)
}
func matchConstellationCode(ra, dec float64) string {
+15 -5
View File
@@ -5,11 +5,21 @@ import (
)
func TestConstellationNameZH(t *testing.T) {
now := GetNowJD()
//finish on 30s
for i := 0.00; i <= 360.00; i += 0.5 {
for j := -90.00; j <= 90.00; j += 0.5 {
ConstellationNameZH(float64(i), float64(j), now)
for _, jde := range []float64{2451545.0, 2461328.5} {
for i := 0.00; i <= 360.00; i += 0.25 {
for j := -90.00; j <= 90.00; j += 0.25 {
if ConstellationNameZH(i, j, jde) == "" {
t.Fatalf("ConstellationNameZH(%v, %v, %v) is empty", i, j, jde)
}
}
}
}
}
func TestConstellationBoundarySliver(t *testing.T) {
for _, dec := range []float64{3.25, 3.5, 3.75, 4.0} {
if got := ConstellationNameZH(278.25, dec, 2461328.5); got != "巨蛇座" && got != "蛇夫座" {
t.Fatalf("ConstellationNameZH(278.25, %v) = %q, want 巨蛇座 or 蛇夫座", dec, got)
}
}
}
+20 -12
View File
@@ -68,20 +68,26 @@ func DecToBo(jde, ra, dec float64) float64 {
/*
* 地心坐标转站心坐标,参数分别为,地心赤经赤纬 纬度经度,jde,离地心位置au
*/
// 站心视差沿用的旧地球椭球,与 geodata 的赤道半径不同,不可混用。
const (
topocentricEarthRadiusKM = 6378.14
topocentricEarthPolarRadiusKM = 6356.755
)
func pcosi(lat, h float64) float64 {
b := 6356.755
a := 6378.14
b := topocentricEarthPolarRadiusKM
a := topocentricEarthRadiusKM
u := ArcTan(b / a * Tan(lat))
//psin=b/a*Sin(u)+h/6378140*Sin(lat);
pcos := Cos(u) + h/6378140.0*Cos(lat)
//psin=b/a*Sin(u)+h/(topocentricEarthRadiusKM*1000)*Sin(lat);
pcos := Cos(u) + h/(topocentricEarthRadiusKM*1000)*Cos(lat)
return pcos
}
func psini(lat, h float64) float64 {
b := 6356.755
a := 6378.14
b := topocentricEarthPolarRadiusKM
a := topocentricEarthRadiusKM
u := ArcTan(b / a * Tan(lat))
psin := b/a*Sin(u) + h/6378140*Sin(lat)
//pcos=Cos(u)+h/6378140*Cos(lat);
psin := b/a*Sin(u) + h/(topocentricEarthRadiusKM*1000)*Sin(lat)
//pcos=Cos(u)+h/(topocentricEarthRadiusKM*1000)*Cos(lat);
return psin
}
@@ -130,18 +136,20 @@ func TopocentricBo(lo, bo, lat, lon, jde, au, h float64) float64 {
return nbo
}
func GXCLo(lo, bo, jde float64) float64 { //光行差修正
// GXCLoWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func GXCLoWithInstance(lo, bo, jde float64, inst *Instance) float64 { //光行差修正
k := 20.49552
sunlo := SunTrueLo(jde)
sunlo := SunTrueLoWithInstance(jde, inst)
e := Earthe(jde)
epi := EarthPI(jde)
tmp := (-k*Cos(sunlo-lo) + e*k*Cos(epi-lo)) / Cos(bo)
return tmp
}
func GXCBo(lo, bo, jde float64) float64 {
// GXCBoWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func GXCBoWithInstance(lo, bo, jde float64, inst *Instance) float64 {
k := 20.49552
sunlo := SunTrueLo(jde)
sunlo := SunTrueLoWithInstance(jde, inst)
e := Earthe(jde)
epi := EarthPI(jde)
tmp := -k * Sin(bo) * (Sin(sunlo-lo) - e*Sin(epi-lo))
+60 -58
View File
@@ -1,6 +1,8 @@
package basic
import "math"
import (
"math"
)
const (
angularDiameterAstronomicalUnitKM = 149597870.7
@@ -30,9 +32,9 @@ func SunSemidiameter(jde float64) float64 {
return SunSemidiameterN(jde, -1)
}
// SunSemidiameterN 太阳视半径(截断版),单位角秒 / truncated apparent solar semidiameter in arcseconds.
func SunSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(sunEquatorialRadiusKM, EarthAwayN(jde, n))
// SunSemidiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func SunSemidiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return angularSemidiameterFromAU(sunEquatorialRadiusKM, EarthAwayNWithInstance(jde, n, inst))
}
// SunDiameter 太阳视直径,单位角秒 / apparent solar diameter in arcseconds.
@@ -40,19 +42,19 @@ func SunDiameter(jde float64) float64 {
return SunDiameterN(jde, -1)
}
// SunDiameterN 太阳视直径(截断版),单位角秒 / truncated apparent solar diameter in arcseconds.
func SunDiameterN(jde float64, n int) float64 {
return 2 * SunSemidiameterN(jde, n)
// SunDiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func SunDiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return 2 * SunSemidiameterNWithInstance(jde, n, inst)
}
// MoonSemidiameter 月亮视半径,单位角秒 / apparent lunar semidiameter in arcseconds.
func MoonSemidiameter(jde float64) float64 {
return MoonSemidiameterN(jde, -1)
// MoonSemidiameterWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonSemidiameterWithInstance(jde float64, inst *Instance) float64 {
return MoonSemidiameterNWithInstance(jde, -1, inst)
}
// MoonSemidiameterN 月亮视半径(截断版),单位角秒 / truncated apparent lunar semidiameter in arcseconds.
func MoonSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterArcsec(moonEquatorialRadiusKM, HMoonAwayN(jde, n))
// MoonSemidiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonSemidiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return angularSemidiameterArcsec(moonEquatorialRadiusKM, HMoonAwayNWithInstance(jde, n, inst))
}
// MoonDiameter 月亮视直径,单位角秒 / apparent lunar diameter in arcseconds.
@@ -60,9 +62,9 @@ func MoonDiameter(jde float64) float64 {
return MoonDiameterN(jde, -1)
}
// MoonDiameterN 月亮视直径(截断版),单位角秒 / truncated apparent lunar diameter in arcseconds.
func MoonDiameterN(jde float64, n int) float64 {
return 2 * MoonSemidiameterN(jde, n)
// MoonDiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonDiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return 2 * MoonSemidiameterNWithInstance(jde, n, inst)
}
// MercurySemidiameter 水星视半径,单位角秒 / apparent Mercury semidiameter in arcseconds.
@@ -70,9 +72,9 @@ func MercurySemidiameter(jde float64) float64 {
return MercurySemidiameterN(jde, -1)
}
// MercurySemidiameterN 水星视半径(截断版),单位角秒 / truncated apparent Mercury semidiameter in arcseconds.
func MercurySemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(mercuryEquatorialRadiusKM, EarthMercuryAwayN(jde, n))
// MercurySemidiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MercurySemidiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return angularSemidiameterFromAU(mercuryEquatorialRadiusKM, EarthMercuryAwayNWithInstance(jde, n, inst))
}
// MercuryDiameter 水星视直径,单位角秒 / apparent Mercury diameter in arcseconds.
@@ -80,9 +82,9 @@ func MercuryDiameter(jde float64) float64 {
return MercuryDiameterN(jde, -1)
}
// MercuryDiameterN 水星视直径(截断版),单位角秒 / truncated apparent Mercury diameter in arcseconds.
func MercuryDiameterN(jde float64, n int) float64 {
return 2 * MercurySemidiameterN(jde, n)
// MercuryDiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MercuryDiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return 2 * MercurySemidiameterNWithInstance(jde, n, inst)
}
// VenusSemidiameter 金星视半径,单位角秒 / apparent Venus semidiameter in arcseconds.
@@ -90,9 +92,9 @@ func VenusSemidiameter(jde float64) float64 {
return VenusSemidiameterN(jde, -1)
}
// VenusSemidiameterN 金星视半径(截断版),单位角秒 / truncated apparent Venus semidiameter in arcseconds.
func VenusSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(venusEquatorialRadiusKM, EarthVenusAwayN(jde, n))
// VenusSemidiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func VenusSemidiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return angularSemidiameterFromAU(venusEquatorialRadiusKM, EarthVenusAwayNWithInstance(jde, n, inst))
}
// VenusDiameter 金星视直径,单位角秒 / apparent Venus diameter in arcseconds.
@@ -100,9 +102,9 @@ func VenusDiameter(jde float64) float64 {
return VenusDiameterN(jde, -1)
}
// VenusDiameterN 金星视直径(截断版),单位角秒 / truncated apparent Venus diameter in arcseconds.
func VenusDiameterN(jde float64, n int) float64 {
return 2 * VenusSemidiameterN(jde, n)
// VenusDiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func VenusDiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return 2 * VenusSemidiameterNWithInstance(jde, n, inst)
}
// MarsSemidiameter 火星视半径,单位角秒 / apparent Mars semidiameter in arcseconds.
@@ -110,9 +112,9 @@ func MarsSemidiameter(jde float64) float64 {
return MarsSemidiameterN(jde, -1)
}
// MarsSemidiameterN 火星视半径(截断版),单位角秒 / truncated apparent Mars semidiameter in arcseconds.
func MarsSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(marsEquatorialRadiusKM, EarthMarsAwayN(jde, n))
// MarsSemidiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MarsSemidiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return angularSemidiameterFromAU(marsEquatorialRadiusKM, EarthMarsAwayNWithInstance(jde, n, inst))
}
// MarsDiameter 火星视直径,单位角秒 / apparent Mars diameter in arcseconds.
@@ -120,9 +122,9 @@ func MarsDiameter(jde float64) float64 {
return MarsDiameterN(jde, -1)
}
// MarsDiameterN 火星视直径(截断版),单位角秒 / truncated apparent Mars diameter in arcseconds.
func MarsDiameterN(jde float64, n int) float64 {
return 2 * MarsSemidiameterN(jde, n)
// MarsDiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MarsDiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return 2 * MarsSemidiameterNWithInstance(jde, n, inst)
}
// JupiterSemidiameter 木星视半径,单位角秒 / apparent Jupiter semidiameter in arcseconds.
@@ -130,9 +132,9 @@ func JupiterSemidiameter(jde float64) float64 {
return JupiterSemidiameterN(jde, -1)
}
// JupiterSemidiameterN 木星视半径(截断版),单位角秒 / truncated apparent Jupiter semidiameter in arcseconds.
func JupiterSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(jupiterEquatorialRadiusKM, EarthJupiterAwayN(jde, n))
// JupiterSemidiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterSemidiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return angularSemidiameterFromAU(jupiterEquatorialRadiusKM, EarthJupiterAwayNWithInstance(jde, n, inst))
}
// JupiterDiameter 木星视直径,单位角秒 / apparent Jupiter diameter in arcseconds.
@@ -140,9 +142,9 @@ func JupiterDiameter(jde float64) float64 {
return JupiterDiameterN(jde, -1)
}
// JupiterDiameterN 木星视直径(截断版),单位角秒 / truncated apparent Jupiter diameter in arcseconds.
func JupiterDiameterN(jde float64, n int) float64 {
return 2 * JupiterSemidiameterN(jde, n)
// JupiterDiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterDiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return 2 * JupiterSemidiameterNWithInstance(jde, n, inst)
}
// SaturnSemidiameter 土星视半径,单位角秒 / apparent Saturn semidiameter in arcseconds.
@@ -150,9 +152,9 @@ func SaturnSemidiameter(jde float64) float64 {
return SaturnSemidiameterN(jde, -1)
}
// SaturnSemidiameterN 土星视半径(截断版),单位角秒 / truncated apparent Saturn semidiameter in arcseconds.
func SaturnSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(saturnEquatorialRadiusKM, EarthSaturnAwayN(jde, n))
// SaturnSemidiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func SaturnSemidiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return angularSemidiameterFromAU(saturnEquatorialRadiusKM, EarthSaturnAwayNWithInstance(jde, n, inst))
}
// SaturnDiameter 土星视直径,单位角秒 / apparent Saturn diameter in arcseconds.
@@ -160,9 +162,9 @@ func SaturnDiameter(jde float64) float64 {
return SaturnDiameterN(jde, -1)
}
// SaturnDiameterN 土星视直径(截断版),单位角秒 / truncated apparent Saturn diameter in arcseconds.
func SaturnDiameterN(jde float64, n int) float64 {
return 2 * SaturnSemidiameterN(jde, n)
// SaturnDiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func SaturnDiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return 2 * SaturnSemidiameterNWithInstance(jde, n, inst)
}
// UranusSemidiameter 天王星视半径,单位角秒 / apparent Uranus semidiameter in arcseconds.
@@ -170,9 +172,9 @@ func UranusSemidiameter(jde float64) float64 {
return UranusSemidiameterN(jde, -1)
}
// UranusSemidiameterN 天王星视半径(截断版),单位角秒 / truncated apparent Uranus semidiameter in arcseconds.
func UranusSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(uranusEquatorialRadiusKM, EarthUranusAwayN(jde, n))
// UranusSemidiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func UranusSemidiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return angularSemidiameterFromAU(uranusEquatorialRadiusKM, EarthUranusAwayNWithInstance(jde, n, inst))
}
// UranusDiameter 天王星视直径,单位角秒 / apparent Uranus diameter in arcseconds.
@@ -180,9 +182,9 @@ func UranusDiameter(jde float64) float64 {
return UranusDiameterN(jde, -1)
}
// UranusDiameterN 天王星视直径(截断版),单位角秒 / truncated apparent Uranus diameter in arcseconds.
func UranusDiameterN(jde float64, n int) float64 {
return 2 * UranusSemidiameterN(jde, n)
// UranusDiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func UranusDiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return 2 * UranusSemidiameterNWithInstance(jde, n, inst)
}
// NeptuneSemidiameter 海王星视半径,单位角秒 / apparent Neptune semidiameter in arcseconds.
@@ -190,9 +192,9 @@ func NeptuneSemidiameter(jde float64) float64 {
return NeptuneSemidiameterN(jde, -1)
}
// NeptuneSemidiameterN 海王星视半径(截断版),单位角秒 / truncated apparent Neptune semidiameter in arcseconds.
func NeptuneSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(neptuneEquatorialRadiusKM, EarthNeptuneAwayN(jde, n))
// NeptuneSemidiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NeptuneSemidiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return angularSemidiameterFromAU(neptuneEquatorialRadiusKM, EarthNeptuneAwayNWithInstance(jde, n, inst))
}
// NeptuneDiameter 海王星视直径,单位角秒 / apparent Neptune diameter in arcseconds.
@@ -200,7 +202,7 @@ func NeptuneDiameter(jde float64) float64 {
return NeptuneDiameterN(jde, -1)
}
// NeptuneDiameterN 海王星视直径(截断版),单位角秒 / truncated apparent Neptune diameter in arcseconds.
func NeptuneDiameterN(jde float64, n int) float64 {
return 2 * NeptuneSemidiameterN(jde, n)
// NeptuneDiameterNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NeptuneDiameterNWithInstance(jde float64, n int, inst *Instance) float64 {
return 2 * NeptuneSemidiameterNWithInstance(jde, n, inst)
}
-1
View File
@@ -14,7 +14,6 @@ type angularDiameterSample struct {
}
func TestAngularDiametersMatchHorizonsBaseline(t *testing.T) {
// Baseline is generated from JPL Horizons by scripts/generate_angular_diameter_baseline.sh.
data, err := os.ReadFile("testdata/angular_diameter_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
+5
View File
@@ -0,0 +1,5 @@
// Package basic 提供底层天文算法、星历实例入口与内置模型口径。
// Package basic provides the low-level astronomical algorithms, ephemeris-instance entry points and built-in model conventions.
// 求值接口的 N 后缀是 n 变体:n < 0 取完整级数;半解析模型下所有 n >= 0 使用同一张快速表;Legacy 按 n 截断。
// The N suffix marks an n-variant: n < 0 uses the full series, every n >= 0 uses one fast table under the semianalytic model, and Legacy truncates by n.
package basic
+1 -1
View File
@@ -11,7 +11,7 @@ import (
const (
EARTH_EQUATORIAL_RADIUS float64 = 6378137.0
EARTH_POLAR_RADIUS float64 = 6356752.3
EARTH_AVERAGE_RADIUS float64 = 6371393.0
EARTH_AVERAGE_RADIUS float64 = 6371008.8
)
// HeightDistance 高度与地平线距离的关系(单位:米)
+1 -1
View File
@@ -27,7 +27,7 @@ func TestHeightDegreeByLatMatchesSphericalArc(t *testing.T) {
func TestHeightDegreeReferenceValue(t *testing.T) {
got := HeightDegree(10000)
want := 3.20801665537668
want := 3.208113256367627
if math.Abs(got-want) > 1e-12 {
t.Fatalf("HeightDegree(10000) = %.15f want %.15f", got, want)
}
+35
View File
@@ -0,0 +1,35 @@
package basic
import (
"b612.me/astro/internal/ephem"
)
// Instance 显式只读星历实例 / explicit read-only ephemeris instance.
type Instance = ephem.Instance
// EphemerisModel 几何位置所用的星历模型 / ephemeris model behind geometric positions.
type EphemerisModel int
const (
// EphemerisLegacy VSOP87 + ELP2000 截断级数,astro_legacy 构建下为默认 / truncated VSOP87 and ELP2000 series, default under astro_legacy.
EphemerisLegacy EphemerisModel = iota
// EphemerisSemianalytic VSOP2013/TOP2013 + ELP/MPP02 结构、按 DE441 重拟的半解析表 / semi-analytic tables refitted to DE441.
EphemerisSemianalytic
)
// SetEphemerisModel 切换全局星历模型,未知值返回 false / switches the global ephemeris model; unknown values return false.
func SetEphemerisModel(m EphemerisModel) bool {
if m != EphemerisLegacy && m != EphemerisSemianalytic {
return false
}
// 依赖星历的缓存都按 ΔT 世代校验,切换时一并递增。
deltaTFnMu.Lock()
if ephem.SetActive(ephem.Model(m)) {
deltaTGeneration++
}
deltaTFnMu.Unlock()
return true
}
// GetEphemerisModel 当前星历模型 / the active ephemeris model.
func GetEphemerisModel() EphemerisModel { return EphemerisModel(ephem.Active()) }
+120
View File
@@ -0,0 +1,120 @@
package basic
import (
"errors"
"math"
"b612.me/astro/internal/ephem"
"b612.me/astro/planet"
)
func instanceValue(instances []*Instance) *Instance {
if len(instances) == 0 {
return nil
}
return instances[0]
}
func instanceModel(inst *Instance) ephem.Model {
if inst == nil {
return ephem.Active()
}
return inst.Model()
}
// PlanetCoordinateWithInstance 指定星历实例的行星日心经纬或距离 / heliocentric planetary coordinate or distance with an explicit instance.
func PlanetCoordinateWithInstance(index, coordinate int, jde float64, inst *Instance) float64 {
return PlanetCoordinateNWithInstance(index, coordinate, jde, -1, inst)
}
// PlanetCoordinateNWithInstance 指定星历实例的行星日心经纬或距离(n 变体) / n-parameterised heliocentric planetary coordinate or distance with an explicit instance.
func PlanetCoordinateNWithInstance(index, coordinate int, jde float64, n int, inst *Instance) float64 {
if inst == nil {
return planet.WherePlanetN(index, coordinate, jde, n)
}
if inst.Model() != ephem.Custom {
return planet.WherePlanetModelN(inst.Model(), index, coordinate, jde, n)
}
value, ok := planetHeliocentricCoordinateWithInstance(index, coordinate, jde, inst)
if !ok {
return math.NaN()
}
return value
}
func boundVectorApparent(target ephem.Body, jde float64, n int, inst *Instance) (lo, bo, tau, distAU float64, ok bool) {
lo, bo, tau, distAU, ok, err := vectorApparentWithInstance(target, jde, n, inst)
if err != nil {
return math.NaN(), math.NaN(), math.NaN(), math.NaN(), true
}
return lo, bo, tau, distAU, ok
}
func instanceStateEcliptic(target ephem.Body, jde float64, inst *Instance) (lo, bo, distAU float64, ok bool) {
return instanceStateEclipticRelative(target, ephem.Earth, jde, jde, inst)
}
func instanceStateEclipticRelative(target, center ephem.Body, stateJDE, eclipticJDE float64, inst *Instance) (lo, bo, distAU float64, ok bool) {
// 几何状态捷径只给自定义引擎,内置模型必须走与包级相同的级数与视位置管线。
if inst == nil || inst.Model() != ephem.Custom {
return 0, 0, 0, false
}
state, _, err := inst.State(target, center, stateJDE)
if err != nil {
return 0, 0, 0, false
}
distanceKM := math.Sqrt(state[0]*state[0] + state[1]*state[1] + state[2]*state[2])
if distanceKM == 0 || math.IsNaN(distanceKM) || math.IsInf(distanceKM, 0) {
return 0, 0, 0, false
}
direction := [3]float64{state[0] / distanceKM, state[1] / distanceKM, state[2] / distanceKM}
lo, bo = icrfToMeanEcliptic(direction, eclipticJDE)
return lo, bo, distanceKM / astronomicalUnitKM, true
}
func icrfToMeanEcliptic(direction [3]float64, jde float64) (lo, bo float64) {
matrix := ltpPBMAT(2000 + (jde-2451545)/365.25)
var equatorial [3]float64
for axis := range equatorial {
equatorial[axis] = matrix[axis][0]*direction[0] + matrix[axis][1]*direction[1] + matrix[axis][2]*direction[2]
}
sine, cosine := math.Sincos(Obliquity1980(jde) * math.Pi / 180)
eclipticY := equatorial[1]*cosine + equatorial[2]*sine
eclipticZ := -equatorial[1]*sine + equatorial[2]*cosine
lo = math.Atan2(eclipticY, equatorial[0]) * 180 / math.Pi
bo = math.Atan2(eclipticZ, math.Hypot(equatorial[0], eclipticY)) * 180 / math.Pi
return math.Mod(lo+720, 360), bo
}
func instanceApparent(target ephem.Body, jde float64, n int, inst *Instance) (lo, bo, tau, distAU float64, ok bool, err error) {
if inst == nil || n >= 0 && inst.Model() != ephem.Custom {
return 0, 0, 0, 0, false, nil
}
return apparentState(target, jde, inst)
}
// apparentState 求视位置并校验;引擎不支持时 ok=false 且 err 为 nil,状态非法时返回 err。
func apparentState(target ephem.Body, jde float64, inst *Instance) (lo, bo, tau, distAU float64, ok bool, err error) {
direction, distanceKM, tau, err := inst.Apparent(target, jde)
if errors.Is(err, ephem.ErrUnsupportedApparent) || inst.Model() != ephem.Custom && errors.Is(err, ephem.ErrNoEngine) {
return 0, 0, 0, 0, false, nil
}
if err != nil {
return 0, 0, 0, 0, false, err
}
norm := math.Hypot(math.Hypot(direction[0], direction[1]), direction[2])
if !isFiniteFloat(distanceKM) || distanceKM <= 0 || !isFiniteFloat(norm) || norm <= 0 || !isFiniteFloat(tau) || tau < 0 {
return 0, 0, 0, 0, false, errors.New("ephem: invalid apparent state")
}
lo, bo = icrfToTrueEcliptic(direction, jde)
// 天文单位须与 vectorApparent 一致,否则实例与包级距离不逐位相等。
return lo, bo, tau, distanceKM / 149597870.7, true, nil
}
func vectorApparentWithInstance(target ephem.Body, jde float64, n int, inst *Instance) (lo, bo, tau, distAU float64, ok bool, err error) {
if inst != nil {
return instanceApparent(target, jde, n, inst)
}
lo, bo, tau, distAU, ok = vectorApparent(target, jde, n)
return lo, bo, tau, distAU, ok, nil
}
+121
View File
@@ -0,0 +1,121 @@
package basic
import (
"math"
"testing"
"b612.me/astro/internal/ephem"
"b612.me/astro/planet"
)
type p3StateEngine struct {
position [3]float64
err error
}
func (engine p3StateEngine) State(target, center ephem.Body, jde float64) ([3]float64, [3]float64, error) {
return engine.position, [3]float64{}, engine.err
}
func (engine p3StateEngine) Coverage(target ephem.Body) (float64, float64, bool) {
return 2400000, 2500000, true
}
type p3ApparentEngine struct {
p3StateEngine
direction [3]float64
distance float64
tau float64
err error
}
func (engine p3ApparentEngine) Apparent(target ephem.Body, jde float64) ([3]float64, float64, float64, error) {
return engine.direction, engine.distance, engine.tau, engine.err
}
func TestP3BuiltInGeometryPreservesTruncation(t *testing.T) {
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
inst := ephem.NewModelInstance(model)
for _, jde := range []float64{2451545, 2461319.5} {
for _, terms := range []int{-1, 0, 16} {
for _, sample := range []struct {
name string
got float64
want float64
}{
{"SunLo", HSunTrueLoNWithInstance(jde, terms, inst), planet.WherePlanetModelN(model, 0, 0, jde, terms)},
{"SunBo", HSunTrueBoNWithInstance(jde, terms, inst), planet.WherePlanetModelN(model, 0, 1, jde, terms)},
{"SunDistance", EarthAwayNWithInstance(jde, terms, inst), planet.WherePlanetModelN(model, 0, 2, jde, terms)},
{"MoonLo", MoonCalcNewNWithInstance(0, jde, terms, inst), planet.WhereMoonModelN(model, 0, jde, terms)},
{"MoonBo", MoonCalcNewNWithInstance(1, jde, terms, inst), planet.WhereMoonModelN(model, 1, jde, terms)},
{"MoonDistance", HMoonAwayNWithInstance(jde, terms, inst), planet.WhereMoonModelN(model, 2, jde, terms)},
} {
if sample.got != sample.want {
t.Errorf("%s model=%d jde=%v n=%d: got %.17g, want %.17g", sample.name, model, jde, terms, sample.got, sample.want)
}
}
}
}
}
}
func TestP3CustomGeometryUsesMeanEcliptic(t *testing.T) {
const jde = 2461319.5
direction := [3]float64{1, 0, 0}
inst := ephem.NewInstance(p3StateEngine{position: [3]float64{astronomicalUnitKM, 0, 0}})
trueLo, wantBo := icrfToTrueEcliptic(direction, jde)
wantLo := trueLo - Nutation2000Bi(jde)
for _, got := range []float64{HSunTrueLoWithInstance(jde, inst), HMoonTrueLoWithInstance(jde, inst), MercuryLWithInstance(jde, inst)} {
if math.Abs(angleDifference(got, wantLo)) > 1e-12 {
t.Errorf("geometric longitude includes nutation: got %.17g, want %.17g", got, wantLo)
}
}
if got := HSunTrueBoWithInstance(jde, inst); math.Abs(got-wantBo) > 1e-12 {
t.Errorf("latitude: got %.17g, want %.17g", got, wantBo)
}
if got := EarthAwayNWithInstance(jde, 16, inst); got != 1 {
t.Errorf("distance: got %v, want 1 AU", got)
}
}
func TestP3CustomGeometryDoesNotFallback(t *testing.T) {
for _, engine := range []ephem.Engine{
nil,
p3StateEngine{err: ephem.ErrOutOfRange},
p3StateEngine{err: ephem.ErrUnsupportedBody},
p3StateEngine{},
p3StateEngine{position: [3]float64{math.NaN(), 0, 0}},
} {
inst := ephem.NewInstance(engine)
for _, got := range []float64{
HSunTrueLoWithInstance(2451545, inst), HSunTrueBoWithInstance(2451545, inst), EarthAwayNWithInstance(2451545, -1, inst),
HMoonTrueLoWithInstance(2451545, inst), HMoonTrueBoWithInstance(2451545, inst), HMoonAwayWithInstance(2451545, inst),
} {
if !math.IsNaN(got) {
t.Errorf("failed custom engine %T fell back to a built-in model: %v", engine, got)
}
}
}
}
func TestP3CustomApparentFailureDoesNotFallback(t *testing.T) {
state := p3StateEngine{position: [3]float64{astronomicalUnitKM, 0, 0}}
for _, engine := range []p3ApparentEngine{
{p3StateEngine: state, err: ephem.ErrOutOfRange},
{p3StateEngine: state, direction: [3]float64{1, 0, 0}, distance: -1},
{p3StateEngine: state, direction: [3]float64{math.NaN(), 0, 0}, distance: 1},
{p3StateEngine: state, distance: 1},
{p3StateEngine: state, direction: [3]float64{1, 0, 0}, distance: 1, tau: math.NaN()},
} {
inst := ephem.NewInstance(engine)
for _, got := range []float64{HSunApparentLoWithInstance(2451545, inst), HMoonApparentLoWithInstance(2451545, inst), MercuryApparentLoWithInstance(2451545, inst)} {
if !math.IsNaN(got) {
t.Errorf("failed apparent engine fell back to geometry: %v", got)
}
}
}
inst := ephem.NewInstance(state)
if got := HSunApparentLoWithInstance(2451545, inst); !isFiniteFloat(got) {
t.Errorf("state-only engine must retain geometric fallback: %v", got)
}
}
+384
View File
@@ -0,0 +1,384 @@
package basic
import (
"math"
"testing"
"b612.me/astro/internal/ephem"
)
type p3ProbeEngine struct {
angle float64
distance float64
}
func (engine p3ProbeEngine) State(target, center ephem.Body, jdTDB float64) (pos, vel [3]float64, err error) {
angle := engine.angle + float64(target-center)*0.01
distance := engine.distance
if distance == 0 {
distance = 1
}
pos[0] = math.Cos(angle) * astronomicalUnitKM * distance
pos[1] = math.Sin(angle) * astronomicalUnitKM * distance
return pos, vel, nil
}
func (engine p3ProbeEngine) Coverage(target ephem.Body) (start, end float64, ok bool) {
return 2400000, 2500000, target != ephem.SSB
}
func (engine p3ProbeEngine) Apparent(target ephem.Body, jdTT float64) (dir [3]float64, distKM, tauDays float64, err error) {
angle := engine.angle + float64(target)*0.01
dir[0] = math.Cos(angle)
dir[1] = math.Sin(angle)
distance := engine.distance
if distance == 0 {
distance = 1
}
return dir, astronomicalUnitKM * distance, 0.005, nil
}
func TestP3SunMoonFunctionsUseExplicitInstances(t *testing.T) {
previous := ephem.Active()
ephem.SetActive(ephem.Legacy)
t.Cleanup(func() { ephem.SetActive(previous) })
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
jd := 2451545.0
sunFirst := HSunApparentLoNWithInstance(jd, -1, first)
sunSecond := HSunApparentLoNWithInstance(jd, -1, second)
if math.Abs(angleDifference(sunFirst, sunSecond)) < 1 {
t.Fatalf("sun apparent longitude did not follow instance: %.6f vs %.6f", sunFirst, sunSecond)
}
moonFirst := HMoonApparentLoNWithInstance(jd, -1, first)
moonSecond := HMoonApparentLoNWithInstance(jd, -1, second)
if math.Abs(angleDifference(moonFirst, moonSecond)) < 1 {
t.Fatalf("moon apparent longitude did not follow instance: %.6f vs %.6f", moonFirst, moonSecond)
}
if got, want := HSunApparentLoN(jd, -1), HSunApparentLoNWithInstance(jd, -1, nil); math.Abs(angleDifference(got, want)) > 1e-12 {
t.Fatalf("nil instance changed legacy-compatible result: %.15f vs %.15f", got, want)
}
}
func TestP3PlanetFunctionsUseExplicitInstances(t *testing.T) {
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
jd := 2451545.0
for _, sample := range []struct {
name string
first float64
second float64
}{
{"MercuryL", MercuryLWithInstance(jd, first), MercuryLWithInstance(jd, second)},
{"VenusL", VenusLWithInstance(jd, first), VenusLWithInstance(jd, second)},
{"MarsL", MarsLWithInstance(jd, first), MarsLWithInstance(jd, second)},
{"JupiterL", JupiterLWithInstance(jd, first), JupiterLWithInstance(jd, second)},
{"SaturnL", SaturnLWithInstance(jd, first), SaturnLWithInstance(jd, second)},
{"UranusL", UranusLWithInstance(jd, first), UranusLWithInstance(jd, second)},
{"NeptuneL", NeptuneLWithInstance(jd, first), NeptuneLWithInstance(jd, second)},
{"MercuryApparentLo", MercuryApparentLoWithInstance(jd, first), MercuryApparentLoWithInstance(jd, second)},
{"NeptuneApparentLo", NeptuneApparentLoWithInstance(jd, first), NeptuneApparentLoWithInstance(jd, second)},
} {
if math.Abs(angleDifference(sample.first, sample.second)) < 1 {
t.Fatalf("%s did not follow explicit instance: %.6f vs %.6f", sample.name, sample.first, sample.second)
}
}
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
inst := ephem.NewModelInstance(model)
for _, sample := range []struct {
name string
value float64
}{
{"MercuryL", MercuryLWithInstance(jd, inst)},
{"MercuryR", MercuryRWithInstance(jd, inst)},
{"MercuryApparentRa", MercuryApparentRaWithInstance(jd, inst)},
{"NeptuneApparentDec", NeptuneApparentDecWithInstance(jd, inst)},
} {
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
}
}
}
previous := ephem.Active()
ephem.SetActive(ephem.Legacy)
t.Cleanup(func() { ephem.SetActive(previous) })
if got, want := MercuryLWithInstance(jd, nil), MercuryL(jd); got != want {
t.Fatalf("nil instance changed MercuryL: %.15f vs %.15f", got, want)
}
if got, want := NeptuneApparentRaWithInstance(jd, nil), NeptuneApparentRa(jd); got != want {
t.Fatalf("nil instance changed NeptuneApparentRa: %.15f vs %.15f", got, want)
}
}
func TestP3MoonPhaseFunctionsUseExplicitInstances(t *testing.T) {
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
jd := 2451545.0
phaseFirst := MoonPhaseWithInstance(jd, first)
phaseSecond := MoonPhaseWithInstance(jd, second)
if math.Abs(phaseFirst-phaseSecond) < 1e-6 {
t.Fatalf("moon phase did not follow explicit instance: %.12f vs %.12f", phaseFirst, phaseSecond)
}
seekFirst := SunMoonSeekWithInstance(jd, 0, first)
seekSecond := SunMoonSeekWithInstance(jd, 0, second)
if math.Abs(angleDifference(seekFirst, seekSecond)) < 1 {
t.Fatalf("sun-moon phase residual did not follow explicit instance: %.6f vs %.6f", seekFirst, seekSecond)
}
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
inst := ephem.NewModelInstance(model)
for _, sample := range []struct {
name string
value float64
}{
{"CalcMoonSHByJDE", CalcMoonSHByJDEWithInstance(jd, 0, inst)},
{"CalcMoonXHByJDE", CalcMoonXHByJDEWithInstance(jd, 0, inst)},
{"MoonPhase", MoonPhaseWithInstance(jd, inst)},
} {
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
}
}
}
if got, want := MoonPhaseWithInstance(jd, nil), MoonPhase(jd); got != want {
t.Fatalf("nil instance changed MoonPhase: %.15f vs %.15f", got, want)
}
if got, want := CalcMoonSHByJDEWithInstance(jd, 0, nil), CalcMoonSHByJDE(jd, 0); got != want {
t.Fatalf("nil instance changed CalcMoonSHByJDE: %.15f vs %.15f", got, want)
}
}
func TestP3SolarTermFunctionsUseExplicitInstances(t *testing.T) {
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
jd := 2451545.0
firstResidual := JQLospecWithInstance(jd, 0, first)
secondResidual := JQLospecWithInstance(jd, 0, second)
if math.Abs(angleDifference(firstResidual, secondResidual)) < 1 {
t.Fatalf("solar-term longitude did not follow explicit instance: %.6f vs %.6f", firstResidual, secondResidual)
}
if GetShiErCiWithInstance(jd, first) == GetShiErCiWithInstance(jd, second) {
t.Fatalf("twelve-division label did not follow explicit instance")
}
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
inst := ephem.NewModelInstance(model)
for _, sample := range []struct {
name string
value float64
}{
{"GetJQTime", GetJQTimeWithInstance(2024, 270, inst)},
{"GetWuHouTime", GetWuHouTimeWithInstance(2024, 0, inst)},
} {
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
}
}
if got := GetJieqiLoopsWithInstance(2024, 3, inst); len(got) != 3 {
t.Fatalf("GetJieqiLoops model %d returned %d values", model, len(got))
}
}
if got, want := JQLospecWithInstance(jd, 0, nil), JQLospec(jd, 0); got != want {
t.Fatalf("nil instance changed JQLospec: %.15f vs %.15f", got, want)
}
if got, want := GetJQTimeWithInstance(2024, 270, nil), GetJQTime(2024, 270); got != want {
t.Fatalf("nil instance changed GetJQTime: %.15f vs %.15f", got, want)
}
}
func TestP3SunObservationFunctionsUseExplicitInstances(t *testing.T) {
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
jd := 2451545.0
firstHeight := SunHeightWithInstance(jd, 121.47, 31.23, 8, first)
secondHeight := SunHeightWithInstance(jd, 121.47, 31.23, 8, second)
if math.Abs(firstHeight-secondHeight) < 1 {
t.Fatalf("sun height did not follow explicit instance: %.6f vs %.6f", firstHeight, secondHeight)
}
firstAzimuth := SunAzimuthWithInstance(jd, 121.47, 31.23, 8, first)
secondAzimuth := SunAzimuthWithInstance(jd, 121.47, 31.23, 8, second)
if math.Abs(angleDifference(firstAzimuth, secondAzimuth)) < 1 {
t.Fatalf("sun azimuth did not follow explicit instance: %.6f vs %.6f", firstAzimuth, secondAzimuth)
}
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
inst := ephem.NewModelInstance(model)
for _, sample := range []struct {
name string
value float64
}{
{"SunTime", SunTimeWithInstance(jd, inst)},
{"CulminationTime", CulminationTimeWithInstance(jd, 121.47, 8, inst)},
{"SunTimeAngle", SunTimeAngleWithInstance(jd, 121.47, 31.23, 8, inst)},
{"SunHeight", SunHeightWithInstance(jd, 121.47, 31.23, 8, inst)},
{"SunAzimuth", SunAzimuthWithInstance(jd, 121.47, 31.23, 8, inst)},
} {
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
}
}
}
if got, want := SunHeightWithInstance(jd, 121.47, 31.23, 8, nil), SunHeight(jd, 121.47, 31.23, 8); got != want {
t.Fatalf("nil instance changed SunHeight: %.15f vs %.15f", got, want)
}
if got, want := SunAzimuthWithInstance(jd, 121.47, 31.23, 8, nil), SunAzimuth(jd, 121.47, 31.23, 8); got != want {
t.Fatalf("nil instance changed SunAzimuth: %.15f vs %.15f", got, want)
}
}
// 显式 Legacy 实例的均时差与中天时刻必须复现包级 Legacy 结果。
func TestSunTimeWithLegacyInstanceMatchesPackageLevel(t *testing.T) {
previous := ephem.Active()
ephem.SetActive(ephem.Legacy)
t.Cleanup(func() { ephem.SetActive(previous) })
inst := ephem.NewModelInstance(ephem.Legacy)
for _, jd := range []float64{2451545.0, 2460384.5, 2460569.5} {
if got, want := SunTimeWithInstance(jd, inst), SunTime(jd); math.Abs(got-want) > 1e-9 {
t.Fatalf("SunTime jd %.1f: instance %.9f h, package %.9f h", jd, got, want)
}
if got, want := SunTimeNWithInstance(jd, 20, inst), SunTimeN(jd, 20); math.Abs(got-want) > 1e-9 {
t.Fatalf("SunTimeN jd %.1f: instance %.9f h, package %.9f h", jd, got, want)
}
if got, want := CulminationTimeWithInstance(jd, 116.4, 8, inst), CulminationTime(jd, 116.4, 8); math.Abs(got-want)*86400 > 1e-3 {
t.Fatalf("CulminationTime jd %.1f: differs by %.3f s", jd, (got-want)*86400)
}
}
}
func TestP3SunMoonAliasFunctionsUseExplicitInstances(t *testing.T) {
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
jd := 2451545.0
if math.Abs(angleDifference(SunApparentRaWithInstance(jd, first), SunApparentRaWithInstance(jd, second))) < 1 {
t.Fatal("SunApparentRa alias did not follow explicit instance")
}
if math.Abs(angleDifference(MoonGeocentricApparentRaWithInstance(jd, first), MoonGeocentricApparentRaWithInstance(jd, second))) < 1 {
t.Fatal("MoonGeocentricApparentRa alias did not follow explicit instance")
}
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
inst := ephem.NewModelInstance(model)
for _, sample := range []struct {
name string
value float64
}{
{"SunApparentRa", SunApparentRaWithInstance(jd, inst)},
{"SunTrueDec", SunTrueDecWithInstance(jd, inst)},
{"MoonGeocentricTrueRa", MoonGeocentricTrueRaWithInstance(jd, inst)},
{"MoonTrueDec", MoonTrueDecWithInstance(jd, inst)},
} {
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
}
}
}
if got, want := SunApparentRaWithInstance(jd, nil), SunApparentRa(jd); got != want {
t.Fatalf("nil instance changed SunApparentRa: %.15f vs %.15f", got, want)
}
if got, want := MoonGeocentricTrueRaWithInstance(jd, nil), HMoonGeocentricTrueRa(jd); got != want {
t.Fatalf("nil instance changed MoonGeocentricTrueRa: %.15f vs %.15f", got, want)
}
}
func TestP3PlanetMagnitudeFunctionsUseExplicitInstances(t *testing.T) {
first := ephem.NewInstance(p3ProbeEngine{angle: 0, distance: 0.8})
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2, distance: 1.2})
jd := 2451545.0
for _, sample := range []struct {
name string
first float64
second float64
}{
{"MercuryMag", MercuryMagWithInstance(jd, first), MercuryMagWithInstance(jd, second)},
{"VenusMag", VenusMagWithInstance(jd, first), VenusMagWithInstance(jd, second)},
{"MarsMag", MarsMagWithInstance(jd, first), MarsMagWithInstance(jd, second)},
{"JupiterMag", JupiterMagWithInstance(jd, first), JupiterMagWithInstance(jd, second)},
{"UranusMag", UranusMagWithInstance(jd, first), UranusMagWithInstance(jd, second)},
{"NeptuneMag", NeptuneMagWithInstance(jd, first), NeptuneMagWithInstance(jd, second)},
} {
if math.Abs(sample.first-sample.second) < 1e-6 {
t.Fatalf("%s did not follow explicit instance: %.12f vs %.12f", sample.name, sample.first, sample.second)
}
}
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
inst := ephem.NewModelInstance(model)
for _, sample := range []struct {
name string
value float64
}{
{"MercuryMag", MercuryMagWithInstance(jd, inst)},
{"JupiterMag", JupiterMagWithInstance(jd, inst)},
{"NeptuneMag", NeptuneMagWithInstance(jd, inst)},
} {
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
}
}
}
if got, want := MercuryMagWithInstance(jd, nil), MercuryMag(jd); got != want {
t.Fatalf("nil instance changed MercuryMag: %.15f vs %.15f", got, want)
}
}
func TestP3SaturnRingFunctionsUseExplicitInstances(t *testing.T) {
first := ephem.NewInstance(p3ProbeEngine{angle: 0, distance: 0.8})
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2, distance: 1.2})
jd := 2451545.0
firstRing := SaturnRingBWithInstance(jd, first)
secondRing := SaturnRingBWithInstance(jd, second)
if math.Abs(firstRing-secondRing) < 1e-6 {
t.Fatalf("Saturn ring opening angle did not follow explicit instance: %.12f vs %.12f", firstRing, secondRing)
}
firstMagnitude := SaturnMagWithInstance(jd, first)
secondMagnitude := SaturnMagWithInstance(jd, second)
if math.Abs(firstMagnitude-secondMagnitude) < 1e-6 {
t.Fatalf("Saturn magnitude did not follow explicit instance: %.12f vs %.12f", firstMagnitude, secondMagnitude)
}
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
inst := ephem.NewModelInstance(model)
for _, sample := range []struct {
name string
value float64
}{
{"SaturnRingB", SaturnRingBWithInstance(jd, inst)},
{"SaturnRingPositionAngle", SaturnRingPositionAngleWithInstance(jd, inst)},
{"SaturnMag", SaturnMagWithInstance(jd, inst)},
} {
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
}
}
}
if got, want := SaturnMagWithInstance(jd, nil), SaturnMag(jd); got != want {
t.Fatalf("nil instance changed SaturnMag: %.15f vs %.15f", got, want)
}
}
func angleDifference(a, b float64) float64 {
difference := math.Mod(a-b+180, 360)
if difference < 0 {
difference += 360
}
return difference - 180
}
+41
View File
@@ -0,0 +1,41 @@
// 被测契约:默认构建以旧级数开始;切换模型使缓存世代递增,重复设置同一模型不递增,未知模型被拒绝。
package basic
import (
"testing"
"b612.me/astro/internal/ephem"
)
func TestEphemerisDefaultIsBuildDefault(t *testing.T) {
if ephem.BuildDefault != ephem.Legacy && ephem.BuildDefault != ephem.Semianalytic {
t.Fatalf("build default %d", ephem.BuildDefault)
}
if GetEphemerisModel() != EphemerisModel(ephem.BuildDefault) {
t.Fatalf("default model %d, build default %d", GetEphemerisModel(), ephem.BuildDefault)
}
}
func TestSetEphemerisModelBumpsGeneration(t *testing.T) {
if SetEphemerisModel(EphemerisModel(99)) {
t.Fatal("unknown model accepted")
}
prev := GetEphemerisModel()
t.Cleanup(func() { SetEphemerisModel(prev) })
SetEphemerisModel(EphemerisLegacy)
g := deltaTGenerationValue()
SetEphemerisModel(EphemerisSemianalytic)
if deltaTGenerationValue() == g {
t.Fatal("generation did not change")
}
g = deltaTGenerationValue()
SetEphemerisModel(EphemerisSemianalytic)
if deltaTGenerationValue() != g {
t.Fatal("setting the same model changed the generation")
}
before := HMoonTrueLo(2451545.0)
SetEphemerisModel(EphemerisLegacy)
if HMoonTrueLo(2451545.0) == before {
t.Fatal("moon longitude did not follow the model switch")
}
}
+85
View File
@@ -0,0 +1,85 @@
// 事件回归基线按星历模型分文件:旧模型用 testdata/<name>.json,新模型用 testdata/semianalytic/<name>.json。
// ASTRO_UPDATE_GOLDEN=1 且当前为新模型时,以旧基线的输入时刻重算并重写新模型基线;旧模型基线从不改写。
package basic
import (
"encoding/json"
"math"
"os"
"path/filepath"
"sort"
"testing"
"b612.me/astro/internal/ephem"
"b612.me/astro/internal/ephemtest"
)
type eventBaselineSample struct {
InputUTC string `json:"input_utc"`
TTJDBits uint64 `json:"tt_jd_bits"`
Events map[string]uint64 `json:"events"`
}
type eventBaselineFile struct {
Samples []eventBaselineSample `json:"samples"`
}
type namedEventFunc struct {
name string
fn func(float64) float64
}
func readEventBaseline(t *testing.T, path string) eventBaselineFile {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var b eventBaselineFile
if err := json.Unmarshal(data, &b); err != nil {
t.Fatal(err)
}
if len(b.Samples) == 0 {
t.Fatalf("empty event baseline %s", path)
}
return b
}
// eventBaselineData 当前模型的基线原始字节;需要时先重写新模型基线。
func eventBaselineData(t *testing.T, legacyPath string, cases []namedEventFunc) []byte {
t.Helper()
path := ephemtest.Golden(legacyPath)
if ephemtest.Update() && ephem.Active() == ephem.Semianalytic {
src := readEventBaseline(t, legacyPath)
out := eventBaselineFile{Samples: make([]eventBaselineSample, len(src.Samples))}
for i, s := range src.Samples {
jd := math.Float64frombits(s.TTJDBits)
ev := make(map[string]uint64, len(cases))
for _, c := range cases {
ev[c.name] = math.Float64bits(c.fn(jd))
}
out.Samples[i] = eventBaselineSample{InputUTC: s.InputUTC, TTJDBits: s.TTJDBits, Events: ev}
}
data, err := json.MarshalIndent(out, "", " ")
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, append(data, '\n'), 0o644); err != nil {
t.Fatal(err)
}
names := make([]string, 0, len(cases))
for _, c := range cases {
names = append(names, c.name)
}
sort.Strings(names)
t.Logf("rewrote %s: %d samples × %d events", path, len(out.Samples), len(names))
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("%v (regenerate with ASTRO_UPDATE_GOLDEN=1 go test)", err)
}
return data
}
+1 -3
View File
@@ -6,9 +6,7 @@ const (
eventNewtonMaxIterations = 24
eventDirectionalSearchIterations = 128
eventRiseSetScanStep = 1.0 / 1440
// stationDerivativeStepDay 「留」精修用的中心差分步长(天)。
// 0.5 秒级的步长会被浮点相消噪声支配(实测外行星留误差可达 42 s);
// 0.01 天(14.4 分钟)实测误差 <0.05 s,且求值次数不变。
// stationDerivativeStepDay 「留」精修用的中心差分步长(天):更细的步长会被浮点相消噪声支配。
stationDerivativeStepDay = 0.01
)
+4 -4
View File
@@ -3,6 +3,8 @@ package basic
import (
"math"
"time"
"b612.me/astro/internal/geodata"
)
// 等时线(食甚/掩甚时刻等值线)的共享常量与工具。
@@ -15,8 +17,6 @@ const (
greatestTimeContourCoverToleranceKM = 10.0
// greatestTimeContourCorrectionBacktracking 是牛顿投影跳出定义域时按二分回退的次数。
greatestTimeContourCorrectionBacktracking = 6
// greatestTimeContourKMPerDegree 是地面大圆每度的近似长度,用于局部平面投影。
greatestTimeContourKMPerDegree = 111.195
// greatestTimeContourMaxLevels 是单个事件允许的等时线条数上限;超出部分按时间截断,
// 避免极长事件请求出上万条曲线。
greatestTimeContourMaxLevels = 64
@@ -107,9 +107,9 @@ func greatestTimeContourUTCToTT(value time.Time) float64 {
func greatestTimeContourPointSegmentKM(longitude, latitude, startLongitude, startLatitude, endLongitude, endLatitude float64) float64 {
cosine := math.Cos(latitude * rad)
toX := func(value float64) float64 {
return normalizeLongitude(value-startLongitude) * rad * cosine * greatestTimeContourKMPerDegree
return geodata.NormalizeLongitude(value-startLongitude) * rad * cosine * geodata.EarthKMPerDegree
}
toY := func(value float64) float64 { return (value - startLatitude) * greatestTimeContourKMPerDegree }
toY := func(value float64) float64 { return (value - startLatitude) * geodata.EarthKMPerDegree }
pointX, pointY := toX(longitude), toY(latitude)
endX, endY := toX(endLongitude), toY(endLatitude)
lengthSquared := endX*endX + endY*endY
+272
View File
@@ -0,0 +1,272 @@
package basic
import (
"math"
"sort"
)
const (
schaeferNightSkyNanolambert = 180.0
schaeferNanolambertToErg = 1.02e-15
schaeferSunMagnitude = -26.74
schaeferZeroMagnitudeRef = -11.05
schaeferMinSeparationDeg = 0.1
schaeferTwilightLowSunDeg = -3.0
schaeferDaylightHighSunDeg = 4.0
schaeferScotopicLimitNL = 1479.1
)
// HeliacalOptions 偕日可见计算参数;零值使用消光系数 0.25 星等/大气质量、目标最低高度 0.5° / heliacal visibility options.
type HeliacalOptions struct {
// ExtinctionCoefficient 大气消光系数(星等/大气质量)/ extinction in magnitudes per airmass.
ExtinctionCoefficient float64
// MinTargetAltitude 目标最低几何高度(度)/ minimum geometric target altitude in degrees.
MinTargetAltitude float64
// Moonlight 为 true 时计入月光散射(Krisciunas–Schaefer);默认不计 / include scattered moonlight when true.
Moonlight bool
}
func (options HeliacalOptions) normalized() HeliacalOptions {
if !(options.ExtinctionCoefficient > 0) {
options.ExtinctionCoefficient = 0.25
}
if !(options.MinTargetAltitude > 0) {
options.MinTargetAltitude = 0.5
}
return options
}
func schaeferAirmass(altitudeDeg float64) float64 {
z := math.Min(90, math.Max(0, 90-altitudeDeg)) * rad
c := math.Cos(z)
return 1 / (c + 0.025*math.Exp(-11*c))
}
func schaeferTransmission(k, airmass float64) float64 { return math.Pow(10, -0.4*k*airmass) }
func schaeferPhase(separationDeg float64) float64 {
s := math.Max(schaeferMinSeparationDeg, separationDeg)
c := math.Cos(s * rad)
return 6.2e7/(s*s) + math.Pow(10, 6.15-s/40) + math.Pow(10, 5.36)*(1.06+c*c)
}
func validExtinction(extinction float64) bool {
return extinction > 0 && !math.IsInf(extinction, 0)
}
// SkyBrightnessNanolambert 无月夜晨昏天光亮度(nL,Schaefer 模型),消光系数须为正,否则返回 NaN / twilight sky brightness without moonlight, Schaefer model.
func SkyBrightnessNanolambert(targetAltitude, sunAltitude, separation, extinction float64) float64 {
if !validExtinction(extinction) {
return math.NaN()
}
k := extinction
targetT := schaeferTransmission(k, schaeferAirmass(targetAltitude))
sunT := schaeferTransmission(k, schaeferAirmass(sunAltitude))
targetZ := math.Min(90, math.Max(0, 90-targetAltitude))
sunZ := math.Min(180, math.Max(0, 90-sunAltitude))
sep := math.Max(schaeferMinSeparationDeg, separation)
scale := math.Pow(10, -0.4*(schaeferSunMagnitude-schaeferZeroMagnitudeRef+43.27))
day := scale * (1 - targetT) * (schaeferPhase(sep)*sunT + 440000*(1-sunT))
twilight := math.Pow(10, -0.4*(schaeferSunMagnitude-schaeferZeroMagnitudeRef-57.5+sunZ-targetZ/(360*k))) *
(1 - targetT) * 100 / sep
sky := math.Min(day, twilight)
switch {
case sunAltitude < schaeferTwilightLowSunDeg:
sky = twilight
case sunAltitude > schaeferDaylightHighSunDeg:
sky = day
}
sz := math.Sin(targetZ * rad)
night := schaeferNightSkyNanolambert * (0.4 + 0.6/math.Sqrt(1-0.96*sz*sz)) * targetT
return sky/schaeferNanolambertToErg + night
}
// LimitingMagnitude 肉眼极限星等(已扣除目标方向的大气消光),消光系数须为正,否则返回 NaN / naked-eye limiting magnitude after extinction at the target altitude.
func LimitingMagnitude(targetAltitude, sunAltitude, separation, extinction float64) float64 {
return limitingMagnitudeForSky(SkyBrightnessNanolambert(targetAltitude, sunAltitude, separation, extinction), targetAltitude, extinction)
}
// MoonlightNanolambert 月光在目标方向造成的天光亮度(nL,Krisciunas–Schaefer);月在地平下为 0,消光系数非正为 NaN /
// sky brightness from scattered moonlight; zero when the Moon is below the horizon.
func MoonlightNanolambert(targetAltitude, moonAltitude, moonSeparation, moonPhaseAngle, extinction float64) float64 {
if !validExtinction(extinction) {
return math.NaN()
}
if moonAltitude <= 0 || targetAltitude <= 0 {
return 0
}
alpha := math.Abs(moonPhaseAngle)
mag := -12.73 + 0.026*alpha + 4e-9*math.Pow(alpha, 4)
illuminance := math.Pow(10, -0.4*(mag+16.57))
rho := math.Max(schaeferMinSeparationDeg, moonSeparation)
c := math.Cos(rho * rad)
scatter := math.Pow(10, 5.36)*(1.06+c*c) + math.Pow(10, 6.15-rho/40)
airmass := func(alt float64) float64 {
s := math.Sin((90 - alt) * rad)
return 1 / math.Sqrt(1-0.96*s*s)
}
return scatter * illuminance * math.Pow(10, -0.4*extinction*airmass(moonAltitude)) *
(1 - math.Pow(10, -0.4*extinction*airmass(targetAltitude)))
}
func limitingMagnitudeForSky(b, targetAltitude, extinction float64) float64 {
c, k := 4.4668359215096e-9, 1.2589254117942e-6
if b < schaeferScotopicLimitNL {
c, k = 1.5848931924611e-10, 0.012589254117942
}
threshold := c * math.Pow(1+math.Sqrt(k*b), 2)
return -16.57 - 2.5*math.Log10(threshold) - extinction*schaeferAirmass(targetAltitude)
}
// HeliacalEventKind 偕日事件类型 / heliacal event kind.
type HeliacalEventKind int
const (
// HeliacalMorningFirst 晨始见(偕日升)/ morning first visibility.
HeliacalMorningFirst HeliacalEventKind = iota
// HeliacalMorningLast 晨末见 / morning last visibility.
HeliacalMorningLast
// HeliacalEveningFirst 昏始见 / evening first visibility.
HeliacalEveningFirst
// HeliacalEveningLast 昏末见(偕日落)/ evening last visibility.
HeliacalEveningLast
)
// HeliacalEvent 偕日事件:JD 为当天最佳可见时刻(UT 儒略日),Margin 为极限星等减目标星等 / heliacal event at the best moment of that twilight.
type HeliacalEvent struct {
Kind HeliacalEventKind
JD float64
Margin float64
TargetAltitude float64
SunAltitude float64
}
// heliacalTarget 返回 TT 时刻目标的视赤经、赤纬(度)与视星等。
type heliacalTarget func(jde float64) (ra, dec, magnitude float64)
type heliacalBest struct {
jd, margin, targetAlt, sunAlt float64
visible bool
}
func heliacalAltitude(ra, dec, lst, lat float64) float64 {
h := (lst - ra) * rad
d, p := dec*rad, lat*rad
return math.Asin(math.Sin(p)*math.Sin(d)+math.Cos(p)*math.Cos(d)*math.Cos(h)) / rad
}
// heliacalTwilight 扫描一次晨(morning)或昏的太阳高度 −18°..0° 时段,取可见余量最大的样本。
// 日月位置在窗口中点取一次,窗口不足两小时,引入的高度误差远小于模型本身。
func heliacalTwilight(dayJD, lon, lat float64, morning bool, target heliacalTarget, options HeliacalOptions, inst *Instance) heliacalBest {
center := dayJD - lon/360 + 0.5
if morning {
center -= 0.25
} else {
center += 0.25
}
jde := UTC2TT(center)
sunRA, sunDec := HSunApparentRaDecWithInstance(jde, inst)
ra, dec, mag := target(jde)
sep := skyAngularDistance(ra, dec, sunRA, sunDec)
best := heliacalBest{margin: math.Inf(-1)}
if math.IsNaN(ra) || math.IsNaN(mag) {
return best
}
var moonRA, moonDec, moonSep, moonPhase float64
if options.Moonlight {
lo, bo := SkyBodyApparentLoBoWithInstance(SkyMoon, jde, inst)
moonRA, moonDec = LoBoToRaDec(jde, lo, bo)
moonSep = skyAngularDistance(ra, dec, moonRA, moonDec)
moonPhase = 180 - skyAngularDistance(moonRA, moonDec, sunRA, sunDec)
}
lst0 := ApparentSiderealTime(UTC2UT1(center))*15 + lon
const step = 2.0 / 1440
for dt := -0.25; dt <= 0.25; dt += step {
lst := lst0 + dt*360.98564736629
sunAlt := heliacalAltitude(sunRA, sunDec, lst, lat)
if sunAlt > 0 || sunAlt < -18 {
continue
}
alt := heliacalAltitude(ra, dec, lst, lat)
if alt < options.MinTargetAltitude {
continue
}
sky := SkyBrightnessNanolambert(alt, sunAlt, sep, options.ExtinctionCoefficient)
if options.Moonlight {
sky += MoonlightNanolambert(alt, heliacalAltitude(moonRA, moonDec, lst, lat), moonSep, moonPhase, options.ExtinctionCoefficient)
}
margin := limitingMagnitudeForSky(sky, alt, options.ExtinctionCoefficient) - mag
if margin > best.margin {
best = heliacalBest{center + dt, margin, alt, sunAlt, margin >= 0}
}
}
return best
}
// heliacalMaxDays 逐日扫描的天数上限(约 2.3 万年),挡住非法跨度造成的近乎无界扫描。
const heliacalMaxDays = 1 << 23
// findHeliacalEvents 逐日扫描晨、昏可见性,从不可见变为可见记“始见”,反之记“末见”(取末个可见日)。
func findHeliacalEvents(startJD, endJD, lon, lat float64, target heliacalTarget, options HeliacalOptions, inst *Instance) []HeliacalEvent {
options = options.normalized()
if !finite(startJD) || !finite(endJD) || !(endJD > startJD) || math.Abs(lat) >= 90 {
return nil
}
day0 := math.Floor(startJD-0.5) + 0.5
span := math.Floor(endJD - day0)
if !(span < heliacalMaxDays) {
return nil
}
days := int(span)
var out []HeliacalEvent
for _, morning := range []bool{true, false} {
first, last := HeliacalEveningFirst, HeliacalEveningLast
if morning {
first, last = HeliacalMorningFirst, HeliacalMorningLast
}
prev := heliacalTwilight(day0-1, lon, lat, morning, target, options, inst)
for i := 0; i <= days; i++ {
cur := heliacalTwilight(day0+float64(i), lon, lat, morning, target, options, inst)
// 事件时刻是采样日内的细化值,可能越过 endJD,所以上界按事件时刻判断,不能按采样日判断。
switch {
case cur.visible && !prev.visible && cur.jd >= startJD && cur.jd <= endJD:
out = append(out, HeliacalEvent{first, cur.jd, cur.margin, cur.targetAlt, cur.sunAlt})
case !cur.visible && prev.visible && prev.jd >= startJD && prev.jd <= endJD:
out = append(out, HeliacalEvent{last, prev.jd, prev.margin, prev.targetAlt, prev.sunAlt})
}
prev = cur
}
}
sortHeliacalEvents(out)
return out
}
func sortHeliacalEvents(events []HeliacalEvent) {
sort.SliceStable(events, func(i, j int) bool { return events[i].JD < events[j].JD })
}
// FindPlanetHeliacalEventsWithInstance 行星偕日事件(UT 儒略日范围,经度东正)/ planet heliacal events in a UT JD range.
func FindPlanetHeliacalEventsWithInstance(body SkyBody, startJD, endJD, lon, lat float64, options HeliacalOptions, inst *Instance) []HeliacalEvent {
if body < SkyMercury || body > SkyNeptune {
return nil
}
index := int(body-SkyMercury) + 1
target := func(jde float64) (float64, float64, float64) {
lo, bo := SkyBodyApparentLoBoWithInstance(body, jde, inst)
ra, dec := LoBoToRaDec(jde, lo, bo)
return ra, dec, planetMagnitude2018(index, jde, inst)
}
return findHeliacalEvents(startJD, endJD, lon, lat, target, options, inst)
}
// FindStarHeliacalEventsWithInstance 恒星偕日事件;magnitude 为恒星 V 星等 / star heliacal events with the star's V magnitude.
func FindStarHeliacalEventsWithInstance(star StarCoordinate, magnitude, startJD, endJD, lon, lat float64, options HeliacalOptions, inst *Instance) []HeliacalEvent {
if math.IsNaN(magnitude) || math.IsInf(magnitude, 0) {
return nil
}
target := func(jde float64) (float64, float64, float64) {
ra, dec := starApparentRaDecGeocentric(jde, star, inst)
return ra, dec, magnitude
}
return findHeliacalEvents(startJD, endJD, lon, lat, target, options, inst)
}
+138
View File
@@ -0,0 +1,138 @@
// 被测契约:天狼星在开罗约 8 月初晨始见;金星下合前后依次出现昏末见与晨始见;极限星等随天光单调变化;偕日事件不得越出请求区间。
package basic
import (
"math"
"testing"
"time"
)
func TestSiriusHeliacalRisingCairo(t *testing.T) {
sirius := StarCoordinate{ID: "Sirius", RA: 101.28715533, Dec: -16.71611586, Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), Frame: CoordinateFrameJ2000,
ProperMotionRACosDecMasPerYear: -546.01, ProperMotionDecMasPerYear: -1223.07}
start := Date2JD(time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC))
end := Date2JD(time.Date(2025, 9, 30, 0, 0, 0, 0, time.UTC))
events := FindStarHeliacalEventsWithInstance(sirius, -1.46, start, end, 31.23, 30.04, HeliacalOptions{}, nil)
var rising *HeliacalEvent
for i := range events {
if events[i].Kind == HeliacalMorningFirst {
rising = &events[i]
break
}
}
if rising == nil {
t.Fatalf("no morning first visibility: %+v", events)
}
when := JD2Date(rising.JD)
t.Logf("Sirius morning first %v margin %.2f alt %.2f sun %.2f", when, rising.Margin, rising.TargetAltitude, rising.SunAltitude)
if when.Month() != time.July && when.Month() != time.August {
t.Errorf("Sirius heliacal rising %v, expected late July to mid August", when)
}
}
func TestVenusHeliacalAroundInferiorConjunction(t *testing.T) {
// 2025-03-23 金星下合。
start := Date2JD(time.Date(2025, 2, 15, 0, 0, 0, 0, time.UTC))
end := Date2JD(time.Date(2025, 4, 30, 0, 0, 0, 0, time.UTC))
events := FindPlanetHeliacalEventsWithInstance(SkyVenus, start, end, 116.4, 39.9, HeliacalOptions{}, nil)
var eveningLast, morningFirst float64
for _, e := range events {
t.Logf("kind %d %v margin %.2f", e.Kind, JD2Date(e.JD), e.Margin)
if e.Kind == HeliacalEveningLast && eveningLast == 0 {
eveningLast = e.JD
}
if e.Kind == HeliacalMorningFirst && morningFirst == 0 {
morningFirst = e.JD
}
}
conj := Date2JD(time.Date(2025, 3, 23, 0, 0, 0, 0, time.UTC))
if eveningLast == 0 || morningFirst == 0 || !(eveningLast < conj && morningFirst > conj) {
t.Errorf("evening last %.1f, morning first %.1f around conjunction %.1f", eveningLast, morningFirst, conj)
}
}
func TestLimitingMagnitudeDarkensWithTwilight(t *testing.T) {
prev := LimitingMagnitude(20, -2, 30, 0.25)
for _, sun := range []float64{-4, -6, -9, -12, -15, -18} {
cur := LimitingMagnitude(20, sun, 30, 0.25)
if !(cur > prev) {
t.Errorf("sun %.0f: limiting magnitude %.2f not fainter than %.2f", sun, cur, prev)
}
prev = cur
}
if prev < 5 || prev > 7.5 {
t.Errorf("dark-sky limiting magnitude at 20° altitude %.2f", prev)
}
}
func TestMoonlightBrightensSky(t *testing.T) {
if MoonlightNanolambert(30, -5, 20, 10, 0.25) != 0 {
t.Error("moon below horizon must contribute nothing")
}
full := MoonlightNanolambert(30, 40, 20, 5, 0.25)
crescent := MoonlightNanolambert(30, 40, 20, 120, 0.25)
if !(full > crescent && crescent > 0 && full > 1000) {
t.Errorf("full %.1f crescent %.1f nL", full, crescent)
}
start := 2460676.5
plain := FindPlanetHeliacalEventsWithInstance(SkyVenus, start, start+400, 116.4, 39.9, HeliacalOptions{}, nil)
lit := FindPlanetHeliacalEventsWithInstance(SkyVenus, start, start+400, 116.4, 39.9, HeliacalOptions{Moonlight: true}, nil)
if len(plain) == 0 || len(lit) == 0 {
t.Fatalf("events %d %d", len(plain), len(lit))
}
}
func TestHeliacalRejectsUnscannableRange(t *testing.T) {
sirius := StarCoordinate{RA: 101.287, Dec: -16.716}
for _, r := range [][2]float64{
{2451545, math.Inf(1)}, {math.Inf(-1), 2451545}, {math.NaN(), 2451545},
{2451545, math.NaN()}, {2451545, 2451545 + 1e10}, {1e300, math.MaxFloat64},
} {
if got := FindStarHeliacalEventsWithInstance(sirius, -1.46, r[0], r[1], 31.23, 30.04, HeliacalOptions{}, nil); got != nil {
t.Errorf("star %v: %d events", r, len(got))
}
if got := FindPlanetHeliacalEventsWithInstance(SkyVenus, r[0], r[1], 116.4, 39.9, HeliacalOptions{}, nil); got != nil {
t.Errorf("planet %v: %d events", r, len(got))
}
}
}
func TestSkyModelRejectsNonPositiveExtinction(t *testing.T) {
for _, k := range []float64{0, -1, math.NaN(), math.Inf(1)} {
if v := SkyBrightnessNanolambert(20, -10, 30, k); !math.IsNaN(v) {
t.Errorf("sky k=%g: %g", k, v)
}
if v := LimitingMagnitude(20, -10, 30, k); !math.IsNaN(v) {
t.Errorf("limit k=%g: %g", k, v)
}
if v := MoonlightNanolambert(30, 40, 20, 5, k); !math.IsNaN(v) {
t.Errorf("moonlight k=%g: %g", k, v)
}
}
}
func TestHeliacalEventsStayWithinRequestedRange(t *testing.T) {
lon, lat := 116.4, 39.9
narrow := 0
for _, body := range []SkyBody{SkyMercury, SkyVenus, SkyMars, SkyJupiter, SkySaturn} {
wide := FindPlanetHeliacalEventsWithInstance(body, 2459000.5, 2460000.5, lon, lat, HeliacalOptions{}, nil)
for _, event := range wide {
// 事件时刻是采样日内的细化值;把 endJD 卡在两者之间,正是越界最容易出现的窗口。
day := math.Floor(event.JD-0.5) + 0.5
offset := event.JD - day
if offset <= 0.1 {
continue
}
start, end := day, day+offset-0.05
narrow++
for _, got := range FindPlanetHeliacalEventsWithInstance(body, start, end, lon, lat, HeliacalOptions{}, nil) {
if got.JD < start || got.JD > end {
t.Fatalf("body %v: event JD %.6f outside [%.6f, %.6f]", body, got.JD, start, end)
}
}
}
}
if narrow == 0 {
t.Fatal("no narrow window was exercised")
}
}
+32
View File
@@ -0,0 +1,32 @@
package basic
import "b612.me/astro/internal/iau2006"
// RotationMatrix 3×3 旋转矩阵,按行存放,作用于列向量 / 3×3 rotation matrix, row-major, acting on column vectors.
type RotationMatrix = iau2006.Matrix
// Nutation2000A IAU 2000A 章动,返回 (黄经章动, 交角章动),单位度;不进入默认计算链 / IAU 2000A nutation in degrees, opt-in only.
func Nutation2000A(jde float64) (float64, float64) {
dpsi, deps := iau2006.Nutation(jde)
return dpsi / 3600, deps / 3600
}
// MeanObliquity2006 IAU 2006 平黄赤交角(度)/ IAU 2006 mean obliquity in degrees.
func MeanObliquity2006(jde float64) float64 { return iau2006.MeanObliquity(jde) / 3600 }
// EquationOfEquinoxes2006 春分点方程主项 Δψ·cos εA(度),按 IAU 2000A 章动 / equation of the equinoxes in degrees, IAU 2000A.
func EquationOfEquinoxes2006(jde float64) float64 { return iau2006.EquationOfEquinoxes(jde) / 3600 }
// Precession2006Matrix J2000 平赤道到历元平赤道的 IAU 2006 岁差矩阵 / IAU 2006 precession matrix.
func Precession2006Matrix(jde float64) RotationMatrix { return iau2006.Precession(jde) }
// Nutation2000AMatrix 历元平赤道到真赤道的 IAU 2000A 章动矩阵 / IAU 2000A nutation matrix.
func Nutation2000AMatrix(jde float64) RotationMatrix { return iau2006.NutationMatrix(jde) }
// FrameBiasMatrix ICRS 到 J2000 平赤道的框架偏差矩阵 / frame bias from ICRS to the J2000 mean equator.
func FrameBiasMatrix() RotationMatrix { return iau2006.FrameBias() }
// BiasPrecessionNutation2006 ICRS 到真赤道真春分点的完整旋转 N·P·B / full ICRS to true-of-date rotation.
func BiasPrecessionNutation2006(jde float64) RotationMatrix {
return iau2006.BiasPrecessionNutation(jde)
}
+25
View File
@@ -0,0 +1,25 @@
// 被测契约:2000A 与默认 2000B 章动差在 1 mas 量级,单位换算正确。
package basic
import (
"math"
"testing"
)
func TestNutation2000ANearDefault(t *testing.T) {
for _, jde := range []float64{2451545.0, 2460676.5, 2415020.0, 2488069.5} {
a1, a2 := Nutation2000A(jde)
b1, b2 := Nutation2000B(jde)
if d := math.Max(math.Abs(a1-b1), math.Abs(a2-b2)) * 3600e3; d > 2 {
t.Errorf("jde %.1f: 2000A vs 2000B differ by %.3f mas", jde, d)
}
}
if d := math.Abs(MeanObliquity2006(2451545.0)*3600 - 84381.406); d > 1e-9 {
t.Errorf("J2000 obliquity off by %g arcsec", d)
}
m := BiasPrecessionNutation2006(2460676.5)
v := m.Apply([3]float64{1, 0, 0})
if n := math.Sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]); math.Abs(n-1) > 1e-14 {
t.Errorf("rotation not orthonormal: %g", n)
}
}
+27 -44
View File
@@ -115,59 +115,34 @@ func JupiterMag(jde float64) float64 {
return FloatRound(mag, 2)
}
func JupiterHeight(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := JupiterApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(hourAngle)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(hourAngle)
return ArcSin(sinHeight)
// JupiterHeightWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterHeightWithInstance(localJD, lon, lat, timezone float64, inst *Instance) float64 {
return planetHeightN(4, localJD, lon, lat, timezone, -1, inst)
}
func JupiterAzimuth(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := JupiterApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 三角转换公式
tanAzimuth := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(dec)*Cos(lat))
azimuth := ArcTan(tanAzimuth)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
}
return azimuth + 180
}
if hourAngle/15 < 12 {
return azimuth + 180
}
return azimuth
// JupiterAzimuthWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterAzimuthWithInstance(localJD, lon, lat, timezone float64, inst *Instance) float64 {
return planetAzimuthN(4, localJD, lon, lat, timezone, -1, inst)
}
func JupiterHourAngle(jd, lon, timezone float64) float64 {
// JupiterHourAngleWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterHourAngleWithInstance(jd, lon, timezone float64, inst *Instance) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - JupiterApparentRa(UTC2TT(jd-timezone/24.0))
hourAngle := siderealLongitude - JupiterApparentRaWithInstance(UTC2TT(jd-timezone/24.0), inst)
if hourAngle < 0 {
hourAngle += 360
}
return hourAngle
}
func JupiterCulminationTime(localJD, lon, timezone float64) float64 {
// JupiterCulminationTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterCulminationTimeWithInstance(localJD, lon, timezone float64, inst *Instance) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-JupiterHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
estimateJD := localJD + Limit360(360-JupiterHourAngleWithInstance(localJD, lon, timezone, inst))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := JupiterHourAngle(localJD, lon, timezone)
currentHourAngle := JupiterHourAngleWithInstance(localJD, lon, timezone, inst)
if currentHourAngle < 180 {
currentHourAngle += 360
}
@@ -185,14 +160,22 @@ func JupiterCulminationTime(localJD, lon, timezone float64) float64 {
return estimateJD
}
func JupiterRiseTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return jupiterRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true)
// JupiterRiseTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterRiseTimeWithInstance(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, inst *Instance) (float64, error) {
return jupiterRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true, inst)
}
func JupiterSetTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return jupiterRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false)
// JupiterSetTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterSetTimeWithInstance(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, inst *Instance) (float64, error) {
return jupiterRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false, inst)
}
func jupiterRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool) (float64, error) {
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, JupiterCulminationTime, JupiterHeight, JupiterApparentDec)
func jupiterRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool, instances ...*Instance) (float64, error) {
inst := instanceValue(instances)
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, func(localJD, lon, timezone float64) float64 {
return JupiterCulminationTimeWithInstance(localJD, lon, timezone, inst)
}, func(localJD, lon, lat, timezone float64) float64 {
return JupiterHeightWithInstance(localJD, lon, lat, timezone, inst)
}, func(jde float64) float64 { return JupiterApparentDecWithInstance(jde, inst) })
}
+101 -59
View File
@@ -14,8 +14,10 @@ const (
jupiterPhaseCoarseTolerance = 30.0 / 86400.0
)
func jupiterSunLongitudeDelta(jde, degree float64, filter bool) float64 {
sub := Limit360(Limit360(JupiterApparentLo(jde)-HSunApparentLo(jde)) - degree)
func jupiterSunLongitudeDelta(jde, degree float64, filter bool, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := Limit360(Limit360(JupiterApparentLoWithInstance(jde, inst)-HSunApparentLoWithInstance(jde, inst)) - degree)
if filter {
if sub > 180 {
sub -= 360
@@ -27,8 +29,10 @@ func jupiterSunLongitudeDelta(jde, degree float64, filter bool) float64 {
return sub
}
func jupiterSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64 {
sub := Limit360(Limit360(JupiterApparentLoN(jde, n)-HSunApparentLoN(jde, n)) - degree)
func jupiterSunLongitudeDeltaN(jde, degree float64, filter bool, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := Limit360(Limit360(JupiterApparentLoNWithInstance(jde, n, inst)-HSunApparentLoNWithInstance(jde, n, inst)) - degree)
if filter {
if sub > 180 {
sub -= 360
@@ -40,8 +44,10 @@ 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)
func jupiterRADerivative(jde, delta float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := JupiterApparentRaWithInstance(jde+delta, inst) - JupiterApparentRaWithInstance(jde-delta, inst)
if sub > 180 {
sub -= 360
}
@@ -51,8 +57,10 @@ func jupiterRADerivative(jde, delta float64) float64 {
return sub / (2 * delta)
}
func jupiterRADerivativeN(jde, delta float64, n int) float64 {
sub := JupiterApparentRaN(jde+delta, n) - JupiterApparentRaN(jde-delta, n)
func jupiterRADerivativeN(jde, delta float64, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := JupiterApparentRaNWithInstance(jde+delta, n, inst) - JupiterApparentRaNWithInstance(jde-delta, n, inst)
if sub > 180 {
sub -= 360
}
@@ -62,13 +70,15 @@ func jupiterRADerivativeN(jde, delta float64, n int) float64 {
return sub / (2 * delta)
}
func jupiterConjunctionFull(jde, degree float64, next uint8) float64 {
func jupiterConjunctionFull(jde, degree float64, next uint8, instances ...*Instance) float64 {
inst := instanceValue(instances)
//0=last 1=next
if !isFiniteFloat(jde) || !isFiniteFloat(degree) {
return math.NaN()
}
daysPerDegree := JUPITER_S_PERIOD / 360
currentDelta := jupiterSunLongitudeDelta(jde, degree, false)
currentDelta := jupiterSunLongitudeDelta(jde, degree, false, inst)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
@@ -78,8 +88,8 @@ func jupiterConjunctionFull(jde, degree float64, next uint8) float64 {
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJDE+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
longitudeDelta := jupiterSunLongitudeDelta(prevJDE, degree, true, inst)
longitudeSlope := (jupiterSunLongitudeDelta(prevJDE+0.000005, degree, true, inst) - jupiterSunLongitudeDelta(prevJDE-0.000005, degree, true, inst)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
@@ -93,13 +103,15 @@ func jupiterConjunctionFull(jde, degree float64, next uint8) float64 {
return TT2UTC(estimateJDE)
}
func jupiterConjunction(jde, degree float64, next uint8) float64 {
func jupiterConjunction(jde, degree float64, next uint8, instances ...*Instance) float64 {
inst := instanceValue(instances)
//0=last 1=next
if !isFiniteFloat(jde) || !isFiniteFloat(degree) {
return math.NaN()
}
daysPerDegree := JUPITER_S_PERIOD / 360
currentDelta := jupiterSunLongitudeDelta(jde, degree, false)
currentDelta := jupiterSunLongitudeDelta(jde, degree, false, inst)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
@@ -109,8 +121,8 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 {
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDeltaN(prevJDE, degree, true, jupiterEventSearchN)
longitudeSlope := (jupiterSunLongitudeDeltaN(prevJDE+0.000005, degree, true, jupiterEventSearchN) - jupiterSunLongitudeDeltaN(prevJDE-0.000005, degree, true, jupiterEventSearchN)) / 0.00001
longitudeDelta := jupiterSunLongitudeDeltaN(prevJDE, degree, true, jupiterEventSearchN, inst)
longitudeSlope := (jupiterSunLongitudeDeltaN(prevJDE+0.000005, degree, true, jupiterEventSearchN, inst) - jupiterSunLongitudeDeltaN(prevJDE-0.000005, degree, true, jupiterEventSearchN, inst)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= jupiterPhaseCoarseTolerance {
@@ -124,8 +136,8 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 {
converged = false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJDE+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
longitudeDelta := jupiterSunLongitudeDelta(prevJDE, degree, true, inst)
longitudeSlope := (jupiterSunLongitudeDelta(prevJDE+0.000005, degree, true, inst) - jupiterSunLongitudeDelta(prevJDE-0.000005, degree, true, inst)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
@@ -139,39 +151,65 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 {
return TT2UTC(estimateJDE)
}
func LastJupiterConjunction(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 0, jupiterConjunction)
// LastJupiterConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastJupiterConjunctionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 0, func(jde, degree float64, next uint8) float64 {
return jupiterConjunction(jde, degree, next, inst)
})
}
func NextJupiterConjunction(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 0, jupiterConjunction)
// NextJupiterConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextJupiterConjunctionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 0, func(jde, degree float64, next uint8) float64 {
return jupiterConjunction(jde, degree, next, inst)
})
}
func LastJupiterOpposition(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 180, jupiterConjunction)
// LastJupiterOppositionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastJupiterOppositionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 180, func(jde, degree float64, next uint8) float64 {
return jupiterConjunction(jde, degree, next, inst)
})
}
func NextJupiterOpposition(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 180, jupiterConjunction)
// NextJupiterOppositionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextJupiterOppositionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 180, func(jde, degree float64, next uint8) float64 {
return jupiterConjunction(jde, degree, next, inst)
})
}
func NextJupiterEasternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 90, jupiterConjunction)
// NextJupiterEasternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextJupiterEasternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 90, func(jde, degree float64, next uint8) float64 {
return jupiterConjunction(jde, degree, next, inst)
})
}
func LastJupiterEasternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 90, jupiterConjunction)
// LastJupiterEasternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastJupiterEasternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 90, func(jde, degree float64, next uint8) float64 {
return jupiterConjunction(jde, degree, next, inst)
})
}
func NextJupiterWesternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 270, jupiterConjunction)
// NextJupiterWesternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextJupiterWesternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 270, func(jde, degree float64, next uint8) float64 {
return jupiterConjunction(jde, degree, next, inst)
})
}
func LastJupiterWesternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 270, jupiterConjunction)
// LastJupiterWesternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastJupiterWesternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 270, func(jde, degree float64, next uint8) float64 {
return jupiterConjunction(jde, degree, next, inst)
})
}
func jupiterRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
func jupiterRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool, instances ...*Instance) float64 {
inst := instanceValue(instances)
if !isFiniteFloat(oppositionJD) {
return math.NaN()
}
@@ -179,103 +217,107 @@ func jupiterRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposit
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
easternQuadratureUT := jupiterConjunction(oppositionTT, 90, 0)
easternQuadratureUT := jupiterConjunction(oppositionTT, 90, 0, inst)
startTT = UTC2TT(easternQuadratureUT)
} else {
westernQuadratureUT := jupiterConjunction(oppositionTT, 270, 1)
westernQuadratureUT := jupiterConjunction(oppositionTT, 270, 1, inst)
endTT = UTC2TT(westernQuadratureUT)
}
bestJDE := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return jupiterRADerivativeN(jd, stationDerivativeStepDay, jupiterEventSearchN)
return jupiterRADerivativeN(jd, stationDerivativeStepDay, jupiterEventSearchN, inst)
}, func(jd float64) float64 {
return jupiterRADerivative(jd, stationDerivativeStepDay)
return jupiterRADerivative(jd, stationDerivativeStepDay, inst)
})
return TT2UTC(bestJDE)
}
func NextJupiterRetrogradeToPrograde(jde float64) float64 {
// NextJupiterRetrogradeToProgradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextJupiterRetrogradeToProgradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false)
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0, inst)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false, inst)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1, inst)
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
date = jupiterRetrogradeAroundOpposition(nextOppositionJD, false)
date = jupiterRetrogradeAroundOpposition(nextOppositionJD, false, inst)
if !isFiniteFloat(date) || !stationUTQueryAfterOrEqual(date, jde) {
return math.NaN()
}
return date
}
func LastJupiterRetrogradeToPrograde(jde float64) float64 {
// LastJupiterRetrogradeToProgradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastJupiterRetrogradeToProgradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false)
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0, inst)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false, inst)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
if !isFiniteFloat(lastOppositionJD) {
return math.NaN()
}
previousOppositionJD := jupiterConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
previousOppositionJD := jupiterConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0, inst)
if !isFiniteFloat(previousOppositionJD) {
return math.NaN()
}
date = jupiterRetrogradeAroundOpposition(previousOppositionJD, false)
date = jupiterRetrogradeAroundOpposition(previousOppositionJD, false, inst)
if !isFiniteFloat(date) || !stationUTQueryBeforeOrEqual(date, jde) {
return math.NaN()
}
return date
}
func NextJupiterProgradeToRetrograde(jde float64) float64 {
// NextJupiterProgradeToRetrogradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextJupiterProgradeToRetrogradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true)
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1, inst)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true, inst)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
followingOppositionJD := jupiterConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
followingOppositionJD := jupiterConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1, inst)
if !isFiniteFloat(followingOppositionJD) {
return math.NaN()
}
date = jupiterRetrogradeAroundOpposition(followingOppositionJD, true)
date = jupiterRetrogradeAroundOpposition(followingOppositionJD, true, inst)
if !isFiniteFloat(date) || !stationUTQueryAfterOrEqual(date, jde) {
return math.NaN()
}
return date
}
func LastJupiterProgradeToRetrograde(jde float64) float64 {
// LastJupiterProgradeToRetrogradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastJupiterProgradeToRetrogradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true)
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1, inst)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true, inst)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0, inst)
if !isFiniteFloat(lastOppositionJD) {
return math.NaN()
}
date = jupiterRetrogradeAroundOpposition(lastOppositionJD, true)
date = jupiterRetrogradeAroundOpposition(lastOppositionJD, true, inst)
if !isFiniteFloat(date) || !stationUTQueryBeforeOrEqual(date, jde) {
return math.NaN()
}
+16 -15
View File
@@ -3,7 +3,6 @@ package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
@@ -29,10 +28,10 @@ func JupiterCentralMeridians(jde float64) JupiterCentralMeridianInfo {
return JupiterCentralMeridiansN(jde, -1)
}
// JupiterCentralMeridiansN 木星 System I/II/III 中央经线(截断版) / truncated Jupiter System I/II/III central meridians.
func JupiterCentralMeridiansN(jde float64, n int) JupiterCentralMeridianInfo {
observations := jupiterPhysicalObservationsN(jde, n)
physical := JupiterPhysicalN(jde, n)
// JupiterCentralMeridiansNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterCentralMeridiansNWithInstance(jde float64, n int, inst *Instance) JupiterCentralMeridianInfo {
observations := jupiterPhysicalObservationsN(jde, n, inst)
physical := JupiterPhysicalNWithInstance(jde, n, inst)
return JupiterCentralMeridianInfo{
SystemI: observations.SystemI,
SystemII: observations.SystemII,
@@ -45,13 +44,15 @@ func JupiterDSDE(jde float64) (ds, de float64) {
return JupiterDSDEN(jde, -1)
}
// JupiterDSDEN 木星 DS/DE 行星中心赤纬(截断版) / truncated Jupiter planetocentric declinations of Sun and Earth.
func JupiterDSDEN(jde float64, n int) (ds, de float64) {
observations := jupiterPhysicalObservationsN(jde, n)
// JupiterDSDENWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterDSDENWithInstance(jde float64, n int, inst *Instance) (ds, de float64) {
observations := jupiterPhysicalObservationsN(jde, n, inst)
return observations.DS, observations.DE
}
func jupiterPhysicalObservationsN(jde float64, n int) jupiterPhysicalObservationInfo {
func jupiterPhysicalObservationsN(jde float64, n int, instances ...*Instance) jupiterPhysicalObservationInfo {
inst := instanceValue(instances)
days := jde - 2433282.5
julianCentury := days / 36525.0
@@ -60,9 +61,9 @@ func jupiterPhysicalObservationsN(jde float64, n int) jupiterPhysicalObservation
w1 := (17.71 + 877.90003539*days) * rad
w2 := (16.838 + 870.27003539*days) * rad
earthLon := planet.WherePlanetN(-1, 0, jde, n)
earthLat := planet.WherePlanetN(-1, 1, jde, n)
earthRadius := planet.WherePlanetN(-1, 2, jde, n)
earthLon := PlanetCoordinateNWithInstance(-1, 0, jde, n, inst)
earthLat := PlanetCoordinateNWithInstance(-1, 1, jde, n, inst)
earthRadius := PlanetCoordinateNWithInstance(-1, 2, jde, n, inst)
delta := 4.0
var jupiterLon float64
@@ -73,9 +74,9 @@ func jupiterPhysicalObservationsN(jde float64, n int) jupiterPhysicalObservation
var z float64
for i := 0; i < 2; i++ {
lightTimeDays := astronomicalUnitLightTimeDays * delta
jupiterLon = planet.WherePlanetN(4, 0, jde-lightTimeDays, n)
jupiterLat = planet.WherePlanetN(4, 1, jde-lightTimeDays, n)
jupiterRadius = planet.WherePlanetN(4, 2, jde-lightTimeDays, n)
jupiterLon = PlanetCoordinateNWithInstance(4, 0, jde-lightTimeDays, n, inst)
jupiterLat = PlanetCoordinateNWithInstance(4, 1, jde-lightTimeDays, n, inst)
jupiterRadius = PlanetCoordinateNWithInstance(4, 2, jde-lightTimeDays, n, inst)
x = jupiterRadius*Cos(jupiterLat)*Cos(jupiterLon) - earthRadius*Cos(earthLat)*Cos(earthLon)
y = jupiterRadius*Cos(jupiterLat)*Sin(jupiterLon) - earthRadius*Cos(earthLat)*Sin(earthLon)
z = jupiterRadius*Sin(jupiterLat) - earthRadius*Sin(earthLat)
+3
View File
@@ -6,9 +6,12 @@ import (
"os"
"testing"
"time"
"b612.me/astro/internal/ephemtest"
)
func TestJupiterCentralMeridiansMeeusExample43A(t *testing.T) {
ephemtest.PinLegacy(t)
got := JupiterCentralMeridians(2448972.50068)
ds, de := JupiterDSDE(2448972.50068)
+48 -36
View File
@@ -1,6 +1,8 @@
package basic
import "math"
import (
"math"
)
const (
jupiterGalileanContactBracketStepDays = 2.0 / 1440.0
@@ -59,28 +61,30 @@ type jupiterGalileanContactGeometry struct {
effectiveRadius float64
}
// LastJupiterGalileanPhenomenonContactEvent 上一次 IMCCE 风格接触事件 / previous IMCCE-style contact event.
func LastJupiterGalileanPhenomenonContactEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonContactEvent {
// LastJupiterGalileanPhenomenonContactEventWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastJupiterGalileanPhenomenonContactEventWithInstance(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType, inst *Instance) JupiterGalileanPhenomenonContactEvent {
return jupiterGalileanPhenomenonContactEventFromEvent(
LastJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType),
LastJupiterGalileanPhenomenonEventWithInstance(jd, satellite, phenomenonType, inst), inst,
)
}
// NextJupiterGalileanPhenomenonContactEvent 下一次 IMCCE 风格接触事件 / next IMCCE-style contact event.
func NextJupiterGalileanPhenomenonContactEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonContactEvent {
// NextJupiterGalileanPhenomenonContactEventWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextJupiterGalileanPhenomenonContactEventWithInstance(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType, inst *Instance) JupiterGalileanPhenomenonContactEvent {
return jupiterGalileanPhenomenonContactEventFromEvent(
NextJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType),
NextJupiterGalileanPhenomenonEventWithInstance(jd, satellite, phenomenonType, inst), inst,
)
}
// ClosestJupiterGalileanPhenomenonContactEvent 最近一次 IMCCE 风格接触事件 / closest IMCCE-style contact event.
func ClosestJupiterGalileanPhenomenonContactEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonContactEvent {
// ClosestJupiterGalileanPhenomenonContactEventWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func ClosestJupiterGalileanPhenomenonContactEventWithInstance(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType, inst *Instance) JupiterGalileanPhenomenonContactEvent {
return jupiterGalileanPhenomenonContactEventFromEvent(
ClosestJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType),
ClosestJupiterGalileanPhenomenonEventWithInstance(jd, satellite, phenomenonType, inst), inst,
)
}
func jupiterGalileanPhenomenonContactEventFromEvent(event JupiterGalileanPhenomenonEvent) JupiterGalileanPhenomenonContactEvent {
func jupiterGalileanPhenomenonContactEventFromEvent(event JupiterGalileanPhenomenonEvent, instances ...*Instance) JupiterGalileanPhenomenonContactEvent {
inst := instanceValue(instances)
if !event.Valid {
return invalidJupiterGalileanPhenomenonContactEvent()
}
@@ -90,11 +94,11 @@ func jupiterGalileanPhenomenonContactEventFromEvent(event JupiterGalileanPhenome
ok bool
)
if event.Type == JupiterGalileanShadowTransit {
disappearance, reappearance, ok = refineJupiterGalileanShadowContactPair(event)
disappearance, reappearance, ok = refineJupiterGalileanShadowContactPair(event, inst)
} else if event.Type == JupiterGalileanEclipse {
disappearance, reappearance, ok = refineJupiterGalileanEclipseContactPair(event)
disappearance, reappearance, ok = refineJupiterGalileanEclipseContactPair(event, inst)
} else {
disappearance, reappearance, ok = refineJupiterGalileanContactPair(event.Greatest, event.Satellite, event.Type)
disappearance, reappearance, ok = refineJupiterGalileanContactPair(event.Greatest, event.Satellite, event.Type, inst)
}
if !ok {
return invalidJupiterGalileanPhenomenonContactEvent()
@@ -113,24 +117,25 @@ func jupiterGalileanPhenomenonContactEventFromEvent(event JupiterGalileanPhenome
func refineJupiterGalileanContactPair(
greatestJD float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
inst := instanceValue(instances)
signedDistance := func(jd float64) float64 {
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType)
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType, inst)
if !ok {
return math.NaN()
}
return geometry.signedDistance
}
disappearanceStartTarget := func(jd float64) float64 {
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType)
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType, inst)
if !ok {
return math.NaN()
}
return geometry.signedDistance - geometry.effectiveRadius
}
insideTarget := func(jd float64) float64 {
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType)
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType, inst)
if !ok {
return math.NaN()
}
@@ -179,8 +184,9 @@ func refineJupiterGalileanContactPair(
}
func refineJupiterGalileanEclipseContactPair(
event JupiterGalileanPhenomenonEvent,
) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
event JupiterGalileanPhenomenonEvent, instances ...*Instance) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
inst := instanceValue(instances)
satelliteRadius := jupiterGalileanSatelliteRadiusJupiterRadii(event.Satellite)
if !isFinite(satelliteRadius) || satelliteRadius <= 0 {
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
@@ -188,28 +194,28 @@ func refineJupiterGalileanEclipseContactPair(
// IMCCE 的 EC.D/EC.F 更接近“目视消失/重现”而不是纯几何接触:
// D 相用半影入段近似,F 相用本影/半影出段的中点近似。
penumbraOuterTarget := func(jd float64) float64 {
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, true)
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, true, inst)
if !ok {
return math.NaN()
}
return signedDistance - satelliteRadius
}
penumbraInnerTarget := func(jd float64) float64 {
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, true)
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, true, inst)
if !ok {
return math.NaN()
}
return signedDistance + satelliteRadius
}
umbraInnerTarget := func(jd float64) float64 {
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, false)
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, false, inst)
if !ok {
return math.NaN()
}
return signedDistance + satelliteRadius
}
umbraOuterTarget := func(jd float64) float64 {
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, false)
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, false, inst)
if !ok {
return math.NaN()
}
@@ -262,17 +268,18 @@ func refineJupiterGalileanEclipseContactPair(
}
func refineJupiterGalileanShadowContactPair(
event JupiterGalileanPhenomenonEvent,
) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
event JupiterGalileanPhenomenonEvent, instances ...*Instance) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
inst := instanceValue(instances)
penumbraMetric := func(jd float64) float64 {
value, ok := jupiterGalileanShadowLimbMetricAt(jd, event.Satellite, true)
value, ok := jupiterGalileanShadowLimbMetricAt(jd, event.Satellite, true, inst)
if !ok {
return math.NaN()
}
return value
}
umbraMetric := func(jd float64) float64 {
value, ok := jupiterGalileanShadowLimbMetricAt(jd, event.Satellite, false)
value, ok := jupiterGalileanShadowLimbMetricAt(jd, event.Satellite, false, inst)
if !ok {
return math.NaN()
}
@@ -459,13 +466,14 @@ func bisectJupiterGalileanContactRoot(
func jupiterGalileanContactGeometryAt(
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) (jupiterGalileanContactGeometry, bool) {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) (jupiterGalileanContactGeometry, bool) {
inst := instanceValue(instances)
if !isFinite(jd) || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
return jupiterGalileanContactGeometry{}, false
}
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
context := newJupiterGalileanObservationContext(evaluationJDE, inst)
if context.jupiterDistance == 0 {
return jupiterGalileanContactGeometry{}, false
}
@@ -523,12 +531,14 @@ func jupiterGalileanContactGeometryAt(
}
}
func jupiterGalileanEclipseSignedDistanceAt(jd float64, satellite int, penumbra bool) (float64, bool) {
func jupiterGalileanEclipseSignedDistanceAt(jd float64, satellite int, penumbra bool, instances ...*Instance) (float64, bool) {
inst := instanceValue(instances)
if !isFinite(jd) || satellite < 1 || satellite > 4 {
return math.NaN(), false
}
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
context := newJupiterGalileanObservationContext(evaluationJDE, inst)
if context.jupiterDistance == 0 {
return math.NaN(), false
}
@@ -586,12 +596,14 @@ func jupiterGalileanPenumbraRadiusJupiterRadii(satellite int, pathLengthAU, sunD
return penumbraRadiusAU / (jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM)
}
func jupiterGalileanShadowLimbMetricAt(jd float64, satellite int, penumbra bool) (float64, bool) {
func jupiterGalileanShadowLimbMetricAt(jd float64, satellite int, penumbra bool, instances ...*Instance) (float64, bool) {
inst := instanceValue(instances)
if !isFinite(jd) || satellite < 1 || satellite > 4 {
return math.NaN(), false
}
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
context := newJupiterGalileanObservationContext(evaluationJDE, inst)
if context.jupiterDistance == 0 {
return math.NaN(), false
}
+74 -68
View File
@@ -1,6 +1,8 @@
package basic
import "math"
import (
"math"
)
const (
jupiterGalileanEventSearchSpanDays = 8 * 365.25
@@ -56,28 +58,22 @@ type jupiterGalileanShadowPoint struct {
yJupiterRadii float64
}
// LastJupiterGalileanPhenomenonEvent 上一次伽利略卫星现象 / previous Galilean-satellite event.
//
// jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func LastJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
// LastJupiterGalileanPhenomenonEventWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastJupiterGalileanPhenomenonEventWithInstance(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType, inst *Instance) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true, inst)
return event
}
// NextJupiterGalileanPhenomenonEvent 下一次伽利略卫星现象 / next Galilean-satellite event.
//
// jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func NextJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
// NextJupiterGalileanPhenomenonEventWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextJupiterGalileanPhenomenonEventWithInstance(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType, inst *Instance) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false, inst)
return event
}
// ClosestJupiterGalileanPhenomenonEvent 最近一次伽利略卫星现象 / closest Galilean-satellite event.
//
// jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func ClosestJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
last, hasLast := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
next, hasNext := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
// ClosestJupiterGalileanPhenomenonEventWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func ClosestJupiterGalileanPhenomenonEventWithInstance(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType, inst *Instance) JupiterGalileanPhenomenonEvent {
last, hasLast := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true, inst)
next, hasNext := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false, inst)
switch {
case hasLast && !hasNext:
return last
@@ -97,14 +93,15 @@ func searchJupiterGalileanPhenomenonEvent(
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
direction int,
includeCurrent bool,
) (JupiterGalileanPhenomenonEvent, bool) {
includeCurrent bool, instances ...*Instance) (JupiterGalileanPhenomenonEvent, bool) {
inst := instanceValue(instances)
if !isFinite(jd) || direction == 0 || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
return invalidJupiterGalileanPhenomenonEvent(), false
}
if sample := jupiterGalileanPhenomenonMetricAt(jd, satellite, phenomenonType); sample.active {
current := findJupiterGalileanPhenomenonEventAround(jd, satellite, phenomenonType)
if sample := jupiterGalileanPhenomenonMetricAt(jd, satellite, phenomenonType, inst); sample.active {
current := findJupiterGalileanPhenomenonEventAround(jd, satellite, phenomenonType, inst)
if current.Valid && includeCurrent {
return current, true
}
@@ -122,16 +119,16 @@ func searchJupiterGalileanPhenomenonEvent(
sign := float64(direction)
prevTime := jd
prevSample := jupiterGalileanPhenomenonMetricAt(prevTime, satellite, phenomenonType)
prevSample := jupiterGalileanPhenomenonMetricAt(prevTime, satellite, phenomenonType, inst)
midTime := jd + sign*stepDays
midSample := jupiterGalileanPhenomenonMetricAt(midTime, satellite, phenomenonType)
midSample := jupiterGalileanPhenomenonMetricAt(midTime, satellite, phenomenonType, inst)
for i := 2; i <= maxSteps; i++ {
nextTime := jd + sign*float64(i)*stepDays
nextSample := jupiterGalileanPhenomenonMetricAt(nextTime, satellite, phenomenonType)
nextSample := jupiterGalileanPhenomenonMetricAt(nextTime, satellite, phenomenonType, inst)
if event, ok := jupiterGalileanIntervalCandidate(
prevTime, nextTime, prevSample, midSample, nextSample,
jd, satellite, phenomenonType, direction, includeCurrent,
jd, satellite, phenomenonType, direction, includeCurrent, inst,
); ok {
return event, true
}
@@ -150,8 +147,9 @@ func jupiterGalileanIntervalCandidate(
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
direction int,
includeCurrent bool,
) (JupiterGalileanPhenomenonEvent, bool) {
includeCurrent bool, instances ...*Instance) (JupiterGalileanPhenomenonEvent, bool) {
inst := instanceValue(instances)
var best JupiterGalileanPhenomenonEvent
found := false
consider := func(event JupiterGalileanPhenomenonEvent) {
@@ -169,17 +167,17 @@ func jupiterGalileanIntervalCandidate(
midSample.metric <= prevSample.metric &&
midSample.metric <= nextSample.metric {
consider(findJupiterGalileanPhenomenonEventAround(
refineJupiterGalileanMetricMinimum(prevTime, nextTime, satellite, phenomenonType),
satellite, phenomenonType,
refineJupiterGalileanMetricMinimum(prevTime, nextTime, satellite, phenomenonType, inst),
satellite, phenomenonType, inst,
))
}
if jupiterGalileanSignedCrosses(prevSample.signed, nextSample.signed) &&
jupiterGalileanNearDisk(prevSample, midSample, nextSample) {
crossing := refineJupiterGalileanSignedCrossing(prevTime, nextTime, satellite, phenomenonType)
crossing := refineJupiterGalileanSignedCrossing(prevTime, nextTime, satellite, phenomenonType, inst)
// 过零点只说明"靠近木星",只在它已贴近视面时按事件尺度细扫一次。
if sample := jupiterGalileanPhenomenonMetricAt(crossing, satellite, phenomenonType); isFinite(sample.metric) &&
if sample := jupiterGalileanPhenomenonMetricAt(crossing, satellite, phenomenonType, inst); isFinite(sample.metric) &&
sample.metric <= jupiterGalileanCrossingProbeMetric {
if event, ok := jupiterGalileanFineScanAround(crossing, satellite, phenomenonType); ok {
if event, ok := jupiterGalileanFineScanAround(crossing, satellite, phenomenonType, inst); ok {
consider(event)
}
}
@@ -210,8 +208,9 @@ func jupiterGalileanNearDisk(prevSample, midSample, nextSample jupiterGalileanMe
func jupiterGalileanFineScanAround(
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) (JupiterGalileanPhenomenonEvent, bool) {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) (JupiterGalileanPhenomenonEvent, bool) {
inst := instanceValue(instances)
// 从过零点向两侧交替外扩,通常几步内就能落进事件窗口。
steps := int(math.Round(jupiterGalileanCrossingProbeDays / jupiterGalileanCrossingProbeStepDays))
for step := 0; step <= steps; step++ {
@@ -220,10 +219,10 @@ func jupiterGalileanFineScanAround(
continue
}
candidate := jd + sign*float64(step)*jupiterGalileanCrossingProbeStepDays
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType).active {
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType, inst).active {
continue
}
if event := findJupiterGalileanPhenomenonEventAround(candidate, satellite, phenomenonType); event.Valid {
if event := findJupiterGalileanPhenomenonEventAround(candidate, satellite, phenomenonType, inst); event.Valid {
return event, true
}
}
@@ -246,16 +245,17 @@ func jupiterGalileanSignedCrosses(first, second float64) bool {
func refineJupiterGalileanSignedCrossing(
left, right float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
leftValue := jupiterGalileanPhenomenonMetricAt(left, satellite, phenomenonType).signed
rightValue := jupiterGalileanPhenomenonMetricAt(right, satellite, phenomenonType).signed
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) float64 {
inst := instanceValue(instances)
leftValue := jupiterGalileanPhenomenonMetricAt(left, satellite, phenomenonType, inst).signed
rightValue := jupiterGalileanPhenomenonMetricAt(right, satellite, phenomenonType, inst).signed
if !isFinite(leftValue) || !isFinite(rightValue) || (leftValue < 0) == (rightValue < 0) {
return (left + right) / 2
}
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
middle := (left + right) / 2
value := jupiterGalileanPhenomenonMetricAt(middle, satellite, phenomenonType).signed
value := jupiterGalileanPhenomenonMetricAt(middle, satellite, phenomenonType, inst).signed
if !isFinite(value) {
return middle
}
@@ -268,8 +268,10 @@ func refineJupiterGalileanSignedCrossing(
return (left + right) / 2
}
func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
sample := jupiterGalileanPhenomenonMetricAt(jd, satellite, phenomenonType)
func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) JupiterGalileanPhenomenonEvent {
inst := instanceValue(instances)
sample := jupiterGalileanPhenomenonMetricAt(jd, satellite, phenomenonType, inst)
if !sample.active {
return invalidJupiterGalileanPhenomenonEvent()
}
@@ -281,7 +283,7 @@ func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenome
inactiveStart := math.NaN()
for i := 0; i < maxBoundarySteps; i++ {
candidate := activeStart - stepDays
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType).active {
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType, inst).active {
inactiveStart = candidate
break
}
@@ -295,7 +297,7 @@ func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenome
inactiveEnd := math.NaN()
for i := 0; i < maxBoundarySteps; i++ {
candidate := activeEnd + stepDays
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType).active {
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType, inst).active {
inactiveEnd = candidate
break
}
@@ -305,14 +307,14 @@ func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenome
return invalidJupiterGalileanPhenomenonEvent()
}
start := refineJupiterGalileanEventStart(inactiveStart, activeStart, satellite, phenomenonType)
end := refineJupiterGalileanEventEnd(activeEnd, inactiveEnd, satellite, phenomenonType)
start := refineJupiterGalileanEventStart(inactiveStart, activeStart, satellite, phenomenonType, inst)
end := refineJupiterGalileanEventEnd(activeEnd, inactiveEnd, satellite, phenomenonType, inst)
if !isFinite(start) || !isFinite(end) || end <= start {
return invalidJupiterGalileanPhenomenonEvent()
}
greatest := refineJupiterGalileanMetricMinimum(start, end, satellite, phenomenonType)
greatestSample := jupiterGalileanPhenomenonMetricAt(greatest, satellite, phenomenonType)
greatest := refineJupiterGalileanMetricMinimum(start, end, satellite, phenomenonType, inst)
greatestSample := jupiterGalileanPhenomenonMetricAt(greatest, satellite, phenomenonType, inst)
if !greatestSample.active {
return invalidJupiterGalileanPhenomenonEvent()
}
@@ -331,22 +333,23 @@ func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenome
func refineJupiterGalileanEventStart(
outsideJD, insideJD float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) float64 {
inst := instanceValue(instances)
if insideJD < outsideJD {
outsideJD, insideJD = insideJD, outsideJD
}
if jupiterGalileanPhenomenonMetricAt(outsideJD, satellite, phenomenonType).active {
if jupiterGalileanPhenomenonMetricAt(outsideJD, satellite, phenomenonType, inst).active {
return math.NaN()
}
if !jupiterGalileanPhenomenonMetricAt(insideJD, satellite, phenomenonType).active {
if !jupiterGalileanPhenomenonMetricAt(insideJD, satellite, phenomenonType, inst).active {
return math.NaN()
}
left := outsideJD
right := insideJD
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
mid := (left + right) / 2
if jupiterGalileanPhenomenonMetricAt(mid, satellite, phenomenonType).active {
if jupiterGalileanPhenomenonMetricAt(mid, satellite, phenomenonType, inst).active {
right = mid
} else {
left = mid
@@ -358,22 +361,23 @@ func refineJupiterGalileanEventStart(
func refineJupiterGalileanEventEnd(
insideJD, outsideJD float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) float64 {
inst := instanceValue(instances)
if outsideJD < insideJD {
insideJD, outsideJD = outsideJD, insideJD
}
if !jupiterGalileanPhenomenonMetricAt(insideJD, satellite, phenomenonType).active {
if !jupiterGalileanPhenomenonMetricAt(insideJD, satellite, phenomenonType, inst).active {
return math.NaN()
}
if jupiterGalileanPhenomenonMetricAt(outsideJD, satellite, phenomenonType).active {
if jupiterGalileanPhenomenonMetricAt(outsideJD, satellite, phenomenonType, inst).active {
return math.NaN()
}
left := insideJD
right := outsideJD
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
mid := (left + right) / 2
if jupiterGalileanPhenomenonMetricAt(mid, satellite, phenomenonType).active {
if jupiterGalileanPhenomenonMetricAt(mid, satellite, phenomenonType, inst).active {
left = mid
} else {
right = mid
@@ -385,8 +389,9 @@ func refineJupiterGalileanEventEnd(
func refineJupiterGalileanMetricMinimum(
jd1, jd2 float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) float64 {
inst := instanceValue(instances)
left := math.Min(jd1, jd2)
right := math.Max(jd1, jd2)
if right-left <= jupiterGalileanEventEpsilonDays {
@@ -395,21 +400,21 @@ func refineJupiterGalileanMetricMinimum(
const phi = 0.6180339887498948482
x1 := right - phi*(right-left)
x2 := left + phi*(right-left)
f1 := jupiterGalileanPhenomenonMetricAt(x1, satellite, phenomenonType).metric
f2 := jupiterGalileanPhenomenonMetricAt(x2, satellite, phenomenonType).metric
f1 := jupiterGalileanPhenomenonMetricAt(x1, satellite, phenomenonType, inst).metric
f2 := jupiterGalileanPhenomenonMetricAt(x2, satellite, phenomenonType, inst).metric
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
if f1 <= f2 {
right = x2
x2 = x1
f2 = f1
x1 = right - phi*(right-left)
f1 = jupiterGalileanPhenomenonMetricAt(x1, satellite, phenomenonType).metric
f1 = jupiterGalileanPhenomenonMetricAt(x1, satellite, phenomenonType, inst).metric
} else {
left = x1
x1 = x2
f1 = f2
x2 = left + phi*(right-left)
f2 = jupiterGalileanPhenomenonMetricAt(x2, satellite, phenomenonType).metric
f2 = jupiterGalileanPhenomenonMetricAt(x2, satellite, phenomenonType, inst).metric
}
}
return (left + right) / 2
@@ -418,8 +423,9 @@ func refineJupiterGalileanMetricMinimum(
func jupiterGalileanPhenomenonMetricAt(
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) jupiterGalileanMetricSample {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) jupiterGalileanMetricSample {
inst := instanceValue(instances)
if !isFinite(jd) || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
return jupiterGalileanMetricSample{
metric: math.Inf(1),
@@ -429,7 +435,7 @@ func jupiterGalileanPhenomenonMetricAt(
}
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
context := newJupiterGalileanObservationContext(evaluationJDE, inst)
if context.jupiterDistance == 0 {
return jupiterGalileanMetricSample{
metric: math.Inf(1),
+6 -4
View File
@@ -1,6 +1,8 @@
package basic
import "math"
import (
"math"
)
const solarRadiusAU = 695700.0 / astronomicalUnitKM
@@ -30,10 +32,10 @@ func JupiterGalileanSatellitePhenomenon(jde float64, satellite int) JupiterGalil
return context.phenomenonForSatellite(satellite - 1)
}
// JupiterGalileanSatellitePhenomena 四颗伽利略卫星瞬时现象 / instantaneous phenomena of the four Galilean satellites.
func JupiterGalileanSatellitePhenomena(jde float64) [4]JupiterGalileanPhenomenon {
// JupiterGalileanSatellitePhenomenaWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterGalileanSatellitePhenomenaWithInstance(jde float64, inst *Instance) [4]JupiterGalileanPhenomenon {
var phenomena [4]JupiterGalileanPhenomenon
context := newJupiterGalileanObservationContext(jde)
context := newJupiterGalileanObservationContext(jde, inst)
for i := range phenomena {
phenomena[i] = context.phenomenonForSatellite(i)
}
+27 -21
View File
@@ -3,7 +3,6 @@ package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
@@ -105,13 +104,10 @@ func JupiterGalileanSatelliteObservation(jde float64, satellite int) JupiterGali
return context.observationForSatellite(satellite - 1)
}
// JupiterGalileanSatelliteObservations 四颗伽利略卫星视位置 / apparent geometry of the four Galilean satellites.
//
// 返回次序固定为 Io、Europa、Ganymede、Callisto。
// The returned order is Io, Europa, Ganymede, Callisto.
func JupiterGalileanSatelliteObservations(jde float64) [4]JupiterGalileanObservation {
// JupiterGalileanSatelliteObservationsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func JupiterGalileanSatelliteObservationsWithInstance(jde float64, inst *Instance) [4]JupiterGalileanObservation {
var observations [4]JupiterGalileanObservation
context := newJupiterGalileanObservationContext(jde)
context := newJupiterGalileanObservationContext(jde, inst)
for i := range observations {
observations[i] = context.observationForSatellite(i)
}
@@ -119,6 +115,8 @@ func JupiterGalileanSatelliteObservations(jde float64) [4]JupiterGalileanObserva
}
type jupiterGalileanObservationContext struct {
inst *Instance
jd float64
targetJD float64
earthHelioJ2000 Vector3
@@ -141,13 +139,15 @@ type jupiterGalileanObservationContext struct {
bodyZ Vector3
}
func newJupiterGalileanObservationContext(jde float64) jupiterGalileanObservationContext {
context := jupiterGalileanObservationContext{jd: jde}
func newJupiterGalileanObservationContext(jde float64, instances ...*Instance) jupiterGalileanObservationContext {
inst := instanceValue(instances)
context := jupiterGalileanObservationContext{inst: inst, jd: jde}
if !isFinite(jde) {
return context
}
context.earthHelioJ2000 = rotateEclipticToEquatorial(earthHeliocentricVectorJ2000(jde), orbitJ2000Obliquity)
context.jupiterGeoJ2000, context.jupiterLightTime = jupiterAstrometricGeocentricVectorJ2000(jde, context.earthHelioJ2000)
context.earthHelioJ2000 = rotateEclipticToEquatorial(earthHeliocentricVectorJ2000(jde, inst), orbitJ2000Obliquity)
context.jupiterGeoJ2000, context.jupiterLightTime = jupiterAstrometricGeocentricVectorJ2000(jde, context.earthHelioJ2000, inst)
context.targetJD = jde - context.jupiterLightTime
context.jupiterDistance = vectorMagnitude(context.jupiterGeoJ2000)
if context.jupiterDistance == 0 {
@@ -158,7 +158,7 @@ func newJupiterGalileanObservationContext(jde float64) jupiterGalileanObservatio
ra, dec := vectorToRaDec(context.lineOfSight)
context.east = Vector3{-Sin(ra), Cos(ra), 0}
context.north = Vector3{-Cos(ra) * Sin(dec), -Sin(ra) * Sin(dec), Cos(dec)}
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(context.targetJD), orbitJ2000Obliquity)
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(context.targetJD, inst), orbitJ2000Obliquity)
context.sunDistanceAU = vectorMagnitude(jupiterHelio)
context.sunDirection = normalizeVector(Vector3{-jupiterHelio[0], -jupiterHelio[1], -jupiterHelio[2]})
context.sunLineOfSight = Vector3{-context.sunDirection[0], -context.sunDirection[1], -context.sunDirection[2]}
@@ -178,7 +178,7 @@ func (context jupiterGalileanObservationContext) observationForSatellite(index i
if index < 0 || index >= 4 || context.jupiterDistance == 0 {
return invalidJupiterGalileanObservation()
}
state, geocentric := jupiterGalileanSatelliteAstrometricGeocentric(index, context.jd, context.jupiterLightTime, context.earthHelioJ2000)
state, geocentric := jupiterGalileanSatelliteAstrometricGeocentric(index, context.jd, context.jupiterLightTime, context.earthHelioJ2000, context.inst)
direction := normalizeVector(geocentric)
ra, dec := vectorToRaDec(direction)
distance := vectorMagnitude(geocentric)
@@ -214,14 +214,16 @@ func tangentPlaneOffsetAngles(target, center, east, north Vector3) (float64, flo
return math.Atan2(vectorDot(target, east), denominator), math.Atan2(vectorDot(target, north), denominator)
}
func jupiterGalileanSatelliteAstrometricGeocentric(index int, jd, initialLightTime float64, earthHelioJ2000 Vector3) (JupiterGalileanState, Vector3) {
func jupiterGalileanSatelliteAstrometricGeocentric(index int, jd, initialLightTime float64, earthHelioJ2000 Vector3, instances ...*Instance) (JupiterGalileanState, Vector3) {
inst := instanceValue(instances)
lightTime := initialLightTime
state := JupiterGalileanState{}
result := Vector3{}
includeSolarLongPeriod := jupiterGalileanUseSolarLongPeriod(jd)
for i := 0; i < 8; i++ {
targetJDE := jd - lightTime
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(targetJDE), orbitJ2000Obliquity)
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(targetJDE, inst), orbitJ2000Obliquity)
state = jupiterGalileanSatelliteStateAtET(targetJDE-jupiterGalileanReferenceJD, index, includeSolarLongPeriod)
result = Vector3{
jupiterHelio[0] + state.X - earthHelioJ2000[0],
@@ -237,11 +239,13 @@ func jupiterGalileanSatelliteAstrometricGeocentric(index int, jd, initialLightTi
return state, result
}
func jupiterAstrometricGeocentricVectorJ2000(jd float64, earthHelioJ2000 Vector3) (Vector3, float64) {
func jupiterAstrometricGeocentricVectorJ2000(jd float64, earthHelioJ2000 Vector3, instances ...*Instance) (Vector3, float64) {
inst := instanceValue(instances)
lightTime := 0.0
result := Vector3{}
for i := 0; i < 8; i++ {
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(jd-lightTime), orbitJ2000Obliquity)
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(jd-lightTime, inst), orbitJ2000Obliquity)
result = Vector3{
jupiterHelio[0] - earthHelioJ2000[0],
jupiterHelio[1] - earthHelioJ2000[1],
@@ -256,12 +260,14 @@ func jupiterAstrometricGeocentricVectorJ2000(jd float64, earthHelioJ2000 Vector3
return result, lightTime
}
func jupiterHeliocentricVectorJ2000(jde float64) Vector3 {
func jupiterHeliocentricVectorJ2000(jde float64, instances ...*Instance) Vector3 {
inst := instanceValue(instances)
return eclipticVectorAtReferenceEpoch(
eclipticCartesian(
planet.WherePlanet(4, 0, jde),
planet.WherePlanet(4, 1, jde),
planet.WherePlanet(4, 2, jde),
PlanetCoordinateWithInstance(4, 0, jde, inst),
PlanetCoordinateWithInstance(4, 1, jde, inst),
PlanetCoordinateWithInstance(4, 2, jde, inst),
),
jde,
orbitReferenceJD,
+198
View File
@@ -0,0 +1,198 @@
package basic
import (
"math"
"time"
)
const (
// 月掩南北界:星迹相对月心是直线,偏离切点 δ 后月缘需高出 R(1/cosδ − 1),8° 时约 17 km,远超月面起伏。
limbGrazeSpanDeg = 8.0
limbGrazeStepDeg = 0.1
// 界线点月缘平移上限;超出视为不收敛并保留圆月面点。
limbLimitMaxShiftKM = 30.0
limbLimitProbeKM = 1.0
limbLimitIterations = 4
// 太阳 / 月球高度低于此值的界线点不做月缘平移。
limbLimitHorizonDeg = 0.5
)
// limbLimitGap 返回站点处的圆月面接触间隙与月缘要求的间隙(同一单位),二者相等即为月缘界线。
type limbLimitGap func(longitude, latitude float64) (gap, target float64, ok bool)
// limbLimitSolve 从圆月面界线点出发,沿间隙梯度做牛顿迭代,使 gap − gap0 = target;失败时返回原点。
// gap0 是原点处的圆月面间隙:界线点与站心间隙零点之间的模型差由它吸收,只平移月缘带来的部分。
func limbLimitSolve(longitude, latitude float64, evaluate limbLimitGap) (float64, float64, bool) {
kmPerDegLat := math.Pi / 180 * topocentricEarthRadiusKM
gap0, _, ok := evaluate(longitude, latitude)
if !ok {
return longitude, latitude, false
}
lon, lat := longitude, latitude
for iteration := 0; iteration < limbLimitIterations; iteration++ {
gap, target, ok := evaluate(lon, lat)
if !ok {
return longitude, latitude, false
}
residual := gap - gap0 - target
kmPerDegLon := kmPerDegLat * math.Cos(lat*rad)
if kmPerDegLon < 1 {
return longitude, latitude, false
}
dLon := limbLimitProbeKM / kmPerDegLon
dLat := limbLimitProbeKM / kmPerDegLat
east, _, okEast := evaluate(lon+dLon, lat)
north, _, okNorth := evaluate(lon, lat+dLat)
if !okEast || !okNorth {
return longitude, latitude, false
}
gx := (east - gap) / limbLimitProbeKM
gy := (north - gap) / limbLimitProbeKM
norm2 := gx*gx + gy*gy
if !finite(norm2) || norm2 <= 0 {
return longitude, latitude, false
}
stepX, stepY := -residual*gx/norm2, -residual*gy/norm2
lon += stepX / kmPerDegLon
lat += stepY / kmPerDegLat
if math.Abs(stepX) < 1e-4 && math.Abs(stepY) < 1e-4 {
break
}
}
shiftKM := math.Hypot((lon-longitude)*kmPerDegLat*math.Cos(latitude*rad), (lat-latitude)*kmPerDegLat)
if !finite(shiftKM) || shiftKM > limbLimitMaxShiftKM || lat > 90 || lat < -90 {
return longitude, latitude, false
}
if lon > 180 {
lon -= 360
} else if lon < -180 {
lon += 360
}
return lon, lat, true
}
func occultationVectorArray(vector occultationPathVector) [3]float64 {
return [3]float64{vector.x, vector.y, vector.z}
}
// occultationLimbCorrectLimitSeries 把月掩外接触南北界平移到月缘掠掩线:
// 星迹在切点附近按直线处理,有效月缘半径取 max R(θ0+δ)·cosδ。只用于外接触(掠掩)界线。
func occultationLimbCorrectLimitSeries(
points []OccultationPathPoint,
contextAt occultationRiseSetContextFunc,
limb LunarLimb,
location *time.Location,
inst *Instance,
) ([]OccultationPathPoint, bool) {
if limb == nil || len(points) == 0 || contextAt == nil {
return points, false
}
applied := false
corrected := append([]OccultationPathPoint(nil), points...)
for index, point := range points {
if math.Abs(point.MoonAltitude) < limbLimitHorizonDeg {
continue
}
tt := centerTimeTT(point.Time)
context := contextAt(tt)
if !context.valid || context.moon.distanceKM <= 0 {
continue
}
frame := newLunarLimbFrame(limb, tt, occultationVectorArray(context.moon.positionKM), inst)
if !frame.valid {
continue
}
evaluate := func(longitude, latitude float64) (float64, float64, bool) {
state := context.stateAt(longitude, latitude)
if !state.valid || !finite(state.contactMetric) {
return 0, 0, false
}
parallaxKM, distanceKM, _ := occultationRiseSetObserverVectors(context.siderealDegrees, longitude, latitude)
moonKM := occultationPathSub(context.moon.positionKM, parallaxKM)
var targetKM occultationPathVector
if context.target.distanceKM > 0 {
targetKM = occultationPathSub(context.target.positionKM, parallaxKM)
} else {
targetKM = context.target.direction
}
view, ok := frame.viewToward(occultationVectorArray(moonKM), occultationVectorArray(targetKM))
if !ok {
return 0, 0, false
}
effective := 0.0
for offset := -limbGrazeSpanDeg; offset <= limbGrazeSpanDeg+1e-9; offset += limbGrazeStepDeg {
radius, ok := frame.radiusAt(view, offset)
if !ok {
return 0, 0, false
}
if value := radius * math.Cos(offset*rad); value > effective {
effective = value
}
}
moonDistanceKM := occultationRiseSetTopocentricDistance(context.moon, distanceKM)
if !finite(moonDistanceKM) || moonDistanceKM <= effective {
return 0, 0, false
}
// 有效半径更大时掠掩线外移:需要的圆月面间隙为两者角半径之差(度)。
target := (angularSemidiameterArcsec(effective, moonDistanceKM) -
angularSemidiameterArcsec(moonEquatorialRadiusKM, moonDistanceKM)) / 3600
return state.contactMetric, target, true
}
longitude, latitude, ok := limbLimitSolve(point.Longitude, point.Latitude, evaluate)
if !ok {
continue
}
if !limbGroundMoved(point.Longitude, point.Latitude, longitude, latitude) {
continue
}
corrected[index].Longitude, corrected[index].Latitude = longitude, latitude
applied = true
state := context.stateAt(longitude, latitude)
if state.valid {
corrected[index].MoonAltitude = state.moonAltitude
}
}
return corrected, applied
}
// limbCorrectCentralLimits 把全食 / 环食带南北界平移到月缘界线。
func (solver solarEclipseSolver) limbCorrectCentralLimits(points []SolarEclipsePathPoint, limb LunarLimb) ([]SolarEclipsePathPoint, bool) {
if limb == nil || len(points) == 0 {
return points, false
}
limbSolver := solver.withLimbParams(limb)
applied := false
corrected := append([]SolarEclipsePathPoint(nil), points...)
for index, point := range points {
// 地平线上的 U1/U4 端点处地面梯度趋于零,平移不可信,保留圆月面点。
if math.Abs(point.SunAltitude) < limbLimitHorizonDeg || !finite(point.JDE) || point.JDE <= 0 {
continue
}
context := solver.localStateContextAt(point.JDE)
limbContext := limbSolver.localStateContextAt(point.JDE)
if limbContext.limbFrame == nil {
continue
}
// 与站心同一判据:月缘内切间隙取自站心状态的等效内半径,目标是让它在界线上为零。
evaluate := func(longitude, latitude float64) (float64, float64, bool) {
lonRad, latRad := longitude*rad, latitude*rad
state := context.stateAt(lonRad, latRad, 0)
limbState := limbContext.stateAt(lonRad, latRad, 0)
if !limbState.limbApplied {
return 0, 0, false
}
gap := state.movingDiskContactState().internalContactGap()
return gap, gap - limbState.movingDiskContactState().internalContactGap(), true
}
longitude, latitude, ok := limbLimitSolve(point.Longitude, point.Latitude, evaluate)
if !ok {
continue
}
corrected[index].Longitude, corrected[index].Latitude = longitude, latitude
// 牛顿迭代可能收敛回原点(常数月缘即如此),只有位移超过噪声容差才算真正修正。
if limbGroundMoved(point.Longitude, point.Latitude, corrected[index].Longitude, corrected[index].Latitude) {
applied = true
}
}
return corrected, applied
}
+35
View File
@@ -0,0 +1,35 @@
package basic
import (
"math"
"b612.me/astro/internal/geodata"
)
// 月缘修正是否生效的统一判据:每种量纲只有一个容差,所有层都经下面的函数判断,避免口径漂移。
const (
// limbAppliedTimeToleranceDays 接触时刻与时长的容差(0.05 秒)。
limbAppliedTimeToleranceDays = 0.05 / 86400.0
// limbAppliedDistanceToleranceKM 地面界线点的平移容差(0.05 km)。
limbAppliedDistanceToleranceKM = 0.05
// limbAppliedMagnitudeTolerance 食分与遮蔽率的容差。
limbAppliedMagnitudeTolerance = 1e-9
)
// limbTimeChanged 报告两个儒略日(或以日为单位的时长)是否相差超过时刻容差;任一非有限时视为未变。
func limbTimeChanged(corrected, original float64) bool {
return finite(corrected) && finite(original) &&
math.Abs(corrected-original) > limbAppliedTimeToleranceDays
}
// limbMagnitudeChanged 报告食分或遮蔽率是否相差超过容差;任一非有限时视为未变。
func limbMagnitudeChanged(corrected, original float64) bool {
return finite(corrected) && finite(original) &&
math.Abs(corrected-original) > limbAppliedMagnitudeTolerance
}
// limbGroundMoved 报告地面点(度)是否平移超过距离容差。
func limbGroundMoved(originalLon, originalLat, correctedLon, correctedLat float64) bool {
longitude := signedAngleDifference(correctedLon, originalLon) * math.Cos(originalLat*rad)
return math.Hypot(longitude, correctedLat-originalLat)*geodata.EarthKMPerDegree > limbAppliedDistanceToleranceKM
}
+64 -10
View File
@@ -1,6 +1,8 @@
package basic
import "math"
import (
"math"
)
// localEphemerisVectorNode is a pair of Cartesian ephemeris samples at one
// TT. Cartesian interpolation avoids right-ascension wraparound at 0/360.
@@ -82,12 +84,14 @@ type solarEclipseLocalEphemeris struct {
nodes []localEphemerisVectorNode
}
func newSolarEclipseLocalEphemeris(center float64) *solarEclipseLocalEphemeris {
func newSolarEclipseLocalEphemeris(center float64, instances ...*Instance) *solarEclipseLocalEphemeris {
inst := instanceValue(instances)
half := solarLocalEphemerisNodeCount / 2
nodes := make([]localEphemerisVectorNode, solarLocalEphemerisNodeCount)
for index := range nodes {
tt := center + float64(index-half)*solarLocalEphemerisStepDays
sun, moon := solarEclipseSunMoonEquatorial(tt)
sun, moon := solarEclipseSunMoonEquatorial(tt, inst)
nodes[index] = localEphemerisVectorNode{
tt: tt,
first: solarEclipseLLRToXYZ(sun[0], sun[1], sun[2]),
@@ -97,6 +101,50 @@ func newSolarEclipseLocalEphemeris(center float64) *solarEclipseLocalEphemeris {
return &solarEclipseLocalEphemeris{nodes: nodes}
}
// solarEclipseLimbAxes 与日月局部星历同一时间网格的月球本体坐标架插值节点。
type solarEclipseLimbAxes struct {
base lunarLimbFrame
nodes []localEphemerisVectorNode
}
func newSolarEclipseLimbAxes(ephemeris *solarEclipseLocalEphemeris, limb LunarLimb, inst *Instance) *solarEclipseLimbAxes {
if ephemeris == nil || limb == nil {
return nil
}
axes := &solarEclipseLimbAxes{nodes: make([]localEphemerisVectorNode, len(ephemeris.nodes))}
for index, node := range ephemeris.nodes {
frame := newLunarLimbFrame(limb, node.tt, node.next, inst)
if !frame.valid {
return nil
}
if index == len(ephemeris.nodes)/2 {
axes.base = frame
}
axes.nodes[index] = localEphemerisVectorNode{tt: node.tt, first: frame.axisX, next: frame.axisZ}
}
return axes
}
func (axes *solarEclipseLimbAxes) frameAt(tt float64) (lunarLimbFrame, bool) {
if axes == nil {
return lunarLimbFrame{}, false
}
axisX, axisZ, ok := interpolateLocalEphemerisVectors(axes.nodes, tt)
if !ok {
return lunarLimbFrame{}, false
}
frame := axes.base
if frame.axisZ, ok = lunarLimbNormalize(axisZ); !ok {
return lunarLimbFrame{}, false
}
// 两轴分别插值后不再严格正交,按 z 轴重新正交化 x 轴。
if frame.axisX, ok = lunarLimbNormalize(lunarLimbReject(axisX, frame.axisZ)); !ok {
return lunarLimbFrame{}, false
}
frame.axisY = lunarLimbCross(frame.axisZ, frame.axisX)
return frame, true
}
func (ephemeris *solarEclipseLocalEphemeris) equatorialAt(tt float64) ([3]float64, [3]float64, bool) {
var empty [3]float64
if ephemeris == nil {
@@ -119,12 +167,14 @@ type starOccultationLocalEphemeris struct {
hasDistance bool
}
func newStarOccultationLocalEphemeris(center float64, star StarCoordinate) *starOccultationLocalEphemeris {
func newStarOccultationLocalEphemeris(center float64, star StarCoordinate, instances ...*Instance) *starOccultationLocalEphemeris {
inst := instanceValue(instances)
half := occultationLocalEphemerisNodeCount / 2
nodes := make([]localEphemerisVectorNode, occultationLocalEphemerisNodeCount)
for index := range nodes {
tt := center + float64(index-half)*occultationLocalEphemerisStepDays
state := starOccultationEphemerisStateAt(tt, star)
state := starOccultationEphemerisStateAt(tt, star, inst)
moon := occultationPathRaDecVector(state.moonRA, state.moonDec, state.moonDistanceKM)
targetDistance := state.starDistanceKM
if targetDistance <= 0 {
@@ -138,12 +188,14 @@ func newStarOccultationLocalEphemeris(center float64, star StarCoordinate) *star
next: [3]float64{target.x, target.y, target.z},
}
}
return newStarOccultationLocalEphemerisFromNodes(center, star, nodes, false)
return newStarOccultationLocalEphemerisFromNodes(center, star, nodes, false, inst)
}
// newStarOccultationLocalEphemerisFromNodes 装配局部星历:距离必须与节点同源,密集分支同样要带上。
func newStarOccultationLocalEphemerisFromNodes(center float64, star StarCoordinate, nodes []localEphemerisVectorNode, dense bool) *starOccultationLocalEphemeris {
_, _, distanceAU := starApparentRaDecDistanceGeocentric(center, star)
func newStarOccultationLocalEphemerisFromNodes(center float64, star StarCoordinate, nodes []localEphemerisVectorNode, dense bool, instances ...*Instance) *starOccultationLocalEphemeris {
inst := instanceValue(instances)
_, _, distanceAU := starApparentRaDecDistanceGeocentric(center, star, inst)
return &starOccultationLocalEphemeris{
star: star, nodes: nodes, dense: dense,
distanceKM: distanceAU * occultationPathAstronomicalUnitKM, hasDistance: distanceAU > 0,
@@ -195,12 +247,14 @@ type planetOccultationLocalEphemeris struct {
dense bool
}
func newPlanetOccultationLocalEphemeris(center float64, config planetOccultationConfig) *planetOccultationLocalEphemeris {
func newPlanetOccultationLocalEphemeris(center float64, config planetOccultationConfig, instances ...*Instance) *planetOccultationLocalEphemeris {
inst := instanceValue(instances)
half := occultationLocalEphemerisNodeCount / 2
nodes := make([]localEphemerisVectorNode, occultationLocalEphemerisNodeCount)
for index := range nodes {
tt := center + float64(index-half)*occultationLocalEphemerisStepDays
state := planetOccultationEphemerisStateAt(tt, config)
state := planetOccultationEphemerisStateAt(tt, config, inst)
moon := occultationPathRaDecVector(state.moonRA, state.moonDec, state.moonDistanceKM)
target := occultationPathRaDecVector(state.planetRA, state.planetDec, state.planetDistanceKM)
nodes[index] = localEphemerisVectorNode{
+66 -46
View File
@@ -1,6 +1,10 @@
package basic
import "math"
import (
"math"
"b612.me/astro/internal/geodata"
)
// LunarEclipseType 表示月食类型。
type LunarEclipseType string
@@ -22,6 +26,8 @@ const (
// 输入 seedJDE 只需要落在目标望月附近,允许相差数天。
type LunarEclipseResult struct {
Type LunarEclipseType
// LunarLimbApplied 表示接触时刻已按月缘剖面修正 / reports whether lunar-limb refinement changed the contacts.
LunarLimbApplied bool
// ShadowModel 是本次使用的影半径模型,决定影半径与食分的具体数值。
// ShadowModel is the shadow-radius model used, which fixes the shadow radii and magnitudes.
ShadowModel LunarEclipseShadowModel
@@ -86,14 +92,11 @@ const (
)
const (
lunarEarthEquatorialRadiusKM = 6378.1366
lunarAstronomicalUnitKM = 1.49597870691e8
// 沿用月食常量:
// - 0.2725076 用于月亮视半径和半影几何
// - 太阳视半径与日食共用标准档常量,太阳视差用常用角秒常量 8.794
lunarMoonRadiusRatio = 0.2725076
lunarMoonRadiusScale = lunarMoonRadiusRatio * lunarEarthEquatorialRadiusKM * 1.0000036
lunarMoonRadiusScale = lunarMoonRadiusRatio * geodata.EarthEquatorialRadiusKM * 1.0000036
lunarSolarParallaxArcsec = 8.794
lunarLongitudeAberration = -3.4e-6
lunarFiniteDifferenceStep = 60.0 / 86400.0
@@ -132,7 +135,15 @@ func LunarEclipseChauvenet(seedJDE float64) LunarEclipseResult {
func lunarEclipse(seedJDE float64, shadowModel lunarEclipseShadowModel) LunarEclipseResult {
fullMoonJDE := CalcMoonSHByJDE(seedJDE, 1)
maximumJDE, state, dxdt, dydt, minimumDistance := refineLunarEclipseMaximum(fullMoonJDE, shadowModel)
return lunarEclipseFromFullMoon(fullMoonJDE, shadowModel, func(jde float64) lunarShadowState {
return computeLunarShadowState(jde, shadowModel)
})
}
type lunarShadowEvaluator func(float64) lunarShadowState
func lunarEclipseFromFullMoon(fullMoonJDE float64, shadowModel lunarEclipseShadowModel, evaluate lunarShadowEvaluator) LunarEclipseResult {
maximumJDE, state, dxdt, dydt, minimumDistance := refineLunarEclipseMaximumWithEvaluator(fullMoonJDE, evaluate)
// 未发生的阶段保持 NaN:Has* 是权威判据,时刻字段不能拿 0 当哨兵。
model := LunarEclipseShadowModelDanjon
@@ -162,11 +173,11 @@ func lunarEclipse(seedJDE float64, shadowModel lunarEclipseShadowModel) LunarEcl
result.Type = LunarEclipsePenumbral
result.HasPenumbral = true
result.Magnitude = rawUmbralMagnitude
result.PenumbralStart = refineLunarEclipseContact(
state, dxdt, dydt, state.moonRadiusRad+state.penumbraRadiusRad, false, shadowModel,
result.PenumbralStart = refineLunarEclipseContactWithEvaluator(
state, dxdt, dydt, state.moonRadiusRad+state.penumbraRadiusRad, false, evaluate,
)
result.PenumbralEnd = refineLunarEclipseContact(
state, dxdt, dydt, state.moonRadiusRad+state.penumbraRadiusRad, true, shadowModel,
result.PenumbralEnd = refineLunarEclipseContactWithEvaluator(
state, dxdt, dydt, state.moonRadiusRad+state.penumbraRadiusRad, true, evaluate,
)
}
@@ -174,22 +185,22 @@ func lunarEclipse(seedJDE float64, shadowModel lunarEclipseShadowModel) LunarEcl
result.Type = LunarEclipsePartial
result.HasPartial = true
result.Magnitude = rawUmbralMagnitude
result.PartialStart = refineLunarEclipseContact(
state, dxdt, dydt, state.moonRadiusRad+state.umbraRadiusRad, false, shadowModel,
result.PartialStart = refineLunarEclipseContactWithEvaluator(
state, dxdt, dydt, state.moonRadiusRad+state.umbraRadiusRad, false, evaluate,
)
result.PartialEnd = refineLunarEclipseContact(
state, dxdt, dydt, state.moonRadiusRad+state.umbraRadiusRad, true, shadowModel,
result.PartialEnd = refineLunarEclipseContactWithEvaluator(
state, dxdt, dydt, state.moonRadiusRad+state.umbraRadiusRad, true, evaluate,
)
}
if minimumDistance <= state.umbraRadiusRad-state.moonRadiusRad {
result.Type = LunarEclipseTotal
result.HasTotal = true
result.TotalStart = refineLunarEclipseContact(
state, dxdt, dydt, state.umbraRadiusRad-state.moonRadiusRad, false, shadowModel,
result.TotalStart = refineLunarEclipseContactWithEvaluator(
state, dxdt, dydt, state.umbraRadiusRad-state.moonRadiusRad, false, evaluate,
)
result.TotalEnd = refineLunarEclipseContact(
state, dxdt, dydt, state.umbraRadiusRad-state.moonRadiusRad, true, shadowModel,
result.TotalEnd = refineLunarEclipseContactWithEvaluator(
state, dxdt, dydt, state.umbraRadiusRad-state.moonRadiusRad, true, evaluate,
)
}
@@ -203,17 +214,23 @@ func refineLunarEclipseMaximum(
seedJDE float64,
shadowModel lunarEclipseShadowModel,
) (float64, lunarShadowState, float64, float64, float64) {
return refineLunarEclipseMaximumWithEvaluator(seedJDE, func(jde float64) lunarShadowState {
return computeLunarShadowState(jde, shadowModel)
})
}
func refineLunarEclipseMaximumWithEvaluator(seedJDE float64, evaluate lunarShadowEvaluator) (float64, lunarShadowState, float64, float64, float64) {
currentJDE := seedJDE
for i := 0; i < 2; i++ {
state := computeLunarShadowState(currentJDE, shadowModel)
nextState := computeLunarShadowState(currentJDE+lunarFiniteDifferenceStep, shadowModel)
state := evaluate(currentJDE)
nextState := evaluate(currentJDE + lunarFiniteDifferenceStep)
dxdt := (nextState.x - state.x) / lunarFiniteDifferenceStep
dydt := (nextState.y - state.y) / lunarFiniteDifferenceStep
denominator := dxdt*dxdt + dydt*dydt
if denominator == 0 {
finalState := computeLunarShadowState(currentJDE, shadowModel)
finalState := evaluate(currentJDE)
return currentJDE, finalState, 0, 0, math.Hypot(finalState.x, finalState.y)
}
@@ -221,8 +238,8 @@ func refineLunarEclipseMaximum(
currentJDE += correction
}
linearState := computeLunarShadowState(currentJDE, shadowModel)
nextLinearState := computeLunarShadowState(currentJDE+lunarFiniteDifferenceStep, shadowModel)
linearState := evaluate(currentJDE)
nextLinearState := evaluate(currentJDE + lunarFiniteDifferenceStep)
dxdt := (nextLinearState.x - linearState.x) / lunarFiniteDifferenceStep
dydt := (nextLinearState.y - linearState.y) / lunarFiniteDifferenceStep
@@ -234,8 +251,8 @@ func refineLunarEclipseMaximum(
correction := -(linearState.x*dxdt + linearState.y*dydt) / denominator
maximumJDE := currentJDE + correction
finalState := computeLunarShadowState(maximumJDE, shadowModel)
nextState := computeLunarShadowState(maximumJDE+lunarFiniteDifferenceStep, shadowModel)
finalState := evaluate(maximumJDE)
nextState := evaluate(maximumJDE + lunarFiniteDifferenceStep)
dxdt = (nextState.x - finalState.x) / lunarFiniteDifferenceStep
dydt = (nextState.y - finalState.y) / lunarFiniteDifferenceStep
@@ -250,12 +267,18 @@ func refineLunarEclipseContact(
afterMaximum bool,
shadowModel lunarEclipseShadowModel,
) float64 {
return refineLunarEclipseContactWithEvaluator(maximumState, dxdt, dydt, boundaryRadius, afterMaximum, func(jde float64) lunarShadowState {
return computeLunarShadowState(jde, shadowModel)
})
}
func refineLunarEclipseContactWithEvaluator(maximumState lunarShadowState, dxdt, dydt, boundaryRadius float64, afterMaximum bool, evaluate lunarShadowEvaluator) float64 {
firstGuess, ok := solveLineCircleContact(maximumState, dxdt, dydt, boundaryRadius, afterMaximum)
if !ok {
return math.NaN()
}
contactState := computeLunarShadowState(firstGuess, shadowModel)
contactState := evaluate(firstGuess)
refinedRadius := boundaryRadius
switch {
@@ -309,16 +332,20 @@ type lunarEclipsePlaneState struct {
}
func lunarEclipsePlaneStateAt(jde float64) lunarEclipsePlaneState {
return lunarEclipsePlaneStateAtWithInstance(jde, nil)
}
func lunarEclipsePlaneStateAtWithInstance(jde float64, inst *Instance) lunarEclipsePlaneState {
julianCentury := (jde - 2451545.0) / 36525.0
sunLongitude := HSunTrueLo(jde)*rad + sunLongitudeAberrationRad(julianCentury)
sunLatitude := HSunTrueBo(jde) * rad
moonLongitude := HMoonTrueLo(jde)*rad + lunarLongitudeAberration
moonLatitude := HMoonTrueBo(jde)*rad + moonLatitudeAberrationRad(julianCentury)
sunLongitude := HSunTrueLoWithInstance(jde, inst)*rad + sunLongitudeAberrationRad(julianCentury)
sunLatitude := HSunTrueBoWithInstance(jde, inst) * rad
moonLongitude := HMoonTrueLoWithInstance(jde, inst)*rad + lunarLongitudeAberration
moonLatitude := HMoonTrueBoWithInstance(jde, inst)*rad + moonLatitudeAberrationRad(julianCentury)
return lunarEclipsePlaneState{
jde: jde,
x: normalizeRadians(moonLongitude+math.Pi-sunLongitude) * math.Cos((moonLatitude-sunLatitude)/2),
x: normalizeSignedRadians(moonLongitude+math.Pi-sunLongitude) * math.Cos((moonLatitude-sunLatitude)/2),
y: moonLatitude + sunLatitude,
}
}
@@ -328,13 +355,17 @@ func lunarEclipsePlaneStateAt(jde float64) lunarEclipsePlaneState {
// 所有内部角量统一使用弧度。影半径模型允许在 Danjon 与 Chauvenet 之间切换,
// 其余月心轨迹与几何求交框架保持一致。
func computeLunarShadowState(jde float64, shadowModel lunarEclipseShadowModel) lunarShadowState {
plane := lunarEclipsePlaneStateAt(jde)
return computeLunarShadowStateWithInstance(jde, shadowModel, nil)
}
moonDistanceKM := HMoonAway(jde)
sunDistanceAU := EarthAway(jde)
func computeLunarShadowStateWithInstance(jde float64, shadowModel lunarEclipseShadowModel, inst *Instance) lunarShadowState {
plane := lunarEclipsePlaneStateAtWithInstance(jde, inst)
moonDistanceKM := HMoonAwayWithInstance(jde, inst)
sunDistanceAU := EarthAwayNWithInstance(jde, -1, inst)
moonRadiusArcsec := lunarMoonRadiusScale * lunarArcsecPerRadian / moonDistanceKM
earthParallaxArcsec := lunarEarthEquatorialRadiusKM / moonDistanceKM * lunarArcsecPerRadian
earthParallaxArcsec := geodata.EarthEquatorialRadiusKM / moonDistanceKM * lunarArcsecPerRadian
solarRadiusArcsec := eclipseSunRadiusStandardArcsec / sunDistanceAU
solarParallaxArcsec := lunarSolarParallaxArcsec / sunDistanceAU
umbraRadiusArcsec, penumbraRadiusArcsec := lunarEclipseShadowRadiiArcsec(
@@ -370,17 +401,6 @@ func lunarEclipseShadowRadiiArcsec(
}
}
func normalizeRadians(angle float64) float64 {
angle = math.Mod(angle, 2*math.Pi)
if angle > math.Pi {
angle -= 2 * math.Pi
}
if angle <= -math.Pi {
angle += 2 * math.Pi
}
return angle
}
func sunLongitudeAberrationRad(julianCentury float64) float64 {
meanAnomaly := -0.043126 + 628.301955*julianCentury - 0.000002732*julianCentury*julianCentury
eccentricity := 0.016708634 - 0.000042037*julianCentury - 0.0000001267*julianCentury*julianCentury
+9 -3
View File
@@ -18,6 +18,8 @@ type LunarEclipseDiagramOptions struct {
// StepDays 是路径采样步长,单位为日;<=0 时使用 5 分钟。
// StepDays is the path sampling step in days; values <= 0 use five minutes.
StepDays float64
// LunarLimb 月缘剖面,nil 为圆月面;只精修接触时刻,月盘仍按圆面绘制 / lunar limb profile for contacts.
LunarLimb LunarLimb
}
// LunarEclipseDiagramPoint 表示月食穿影图上的一个月心位置。
@@ -88,8 +90,12 @@ func lunarEclipseDiagram(
shadowModel lunarEclipseShadowModel,
options LunarEclipseDiagramOptions,
) LunarEclipseDiagramResult {
return lunarEclipseDiagramWithInstance(seedJDE, shadowModel, options, nil)
}
func lunarEclipseDiagramWithInstance(seedJDE float64, shadowModel lunarEclipseShadowModel, options LunarEclipseDiagramOptions, inst *Instance) LunarEclipseDiagramResult {
options = normalizeLunarEclipseDiagramOptions(options)
eclipse := lunarEclipse(seedJDE, shadowModel)
eclipse := LunarEclipseWithLunarLimb(lunarEclipseWithInstance(seedJDE, shadowModel, inst), options.LunarLimb, inst)
result := LunarEclipseDiagramResult{
Eclipse: eclipse,
StepDays: options.StepDays,
@@ -98,7 +104,7 @@ func lunarEclipseDiagram(
return result
}
maximumState := computeLunarShadowState(eclipse.Maximum, shadowModel)
maximumState := computeLunarShadowStateWithInstance(eclipse.Maximum, shadowModel, inst)
if maximumState.moonRadiusRad <= 0 {
return result
}
@@ -111,7 +117,7 @@ func lunarEclipseDiagram(
result.StepDays = stepDays
result.Points = make([]LunarEclipseDiagramPoint, 0, len(times))
for _, item := range times {
plane := lunarEclipsePlaneStateAt(item.jde)
plane := lunarEclipsePlaneStateAtWithInstance(item.jde, inst)
result.Points = append(result.Points, LunarEclipseDiagramPoint{
JDE: item.jde,
X: plane.x / maximumState.moonRadiusRad,
+61
View File
@@ -0,0 +1,61 @@
package basic
import ()
// LunarEclipseWithInstance 指定星历实例的月食(TT,Danjon),计算失败时食甚为 NaN / lunar eclipse with an explicit instance; TT maximum is NaN on failure.
func LunarEclipseWithInstance(seedJDE float64, inst *Instance) LunarEclipseResult {
return LunarEclipseDanjonWithInstance(seedJDE, inst)
}
// LunarEclipseDanjonWithInstance 指定星历实例的 Danjon 月食 / Danjon lunar eclipse with an explicit ephemeris instance.
func LunarEclipseDanjonWithInstance(seedJDE float64, inst *Instance) LunarEclipseResult {
return lunarEclipseWithInstance(seedJDE, lunarEclipseShadowDanjon, inst)
}
// LunarEclipseChauvenetWithInstance 指定星历实例的 Chauvenet 月食 / Chauvenet lunar eclipse with an explicit ephemeris instance.
func LunarEclipseChauvenetWithInstance(seedJDE float64, inst *Instance) LunarEclipseResult {
return lunarEclipseWithInstance(seedJDE, lunarEclipseShadowChauvenet, inst)
}
func lunarEclipseWithInstance(seedJDE float64, shadowModel lunarEclipseShadowModel, inst *Instance) LunarEclipseResult {
if inst == nil {
return lunarEclipse(seedJDE, shadowModel)
}
fullMoonJDE := CalcMoonSHByJDEWithInstance(seedJDE, 1, inst)
return lunarEclipseFromFullMoon(fullMoonJDE, shadowModel, func(jde float64) lunarShadowState {
return computeLunarShadowStateWithInstance(jde, shadowModel, inst)
})
}
// LunarEclipseShadowGeometryAtWithInstance 指定星历实例的 Danjon 地影几何 / Danjon shadow geometry with an explicit ephemeris instance.
func LunarEclipseShadowGeometryAtWithInstance(maximumJDE float64, inst *Instance) LunarEclipseShadowGeometry {
return lunarEclipseShadowGeometryAtWithInstance(maximumJDE, lunarEclipseShadowDanjon, inst)
}
// LunarEclipseShadowGeometryChauvenetAtWithInstance 指定星历实例的 Chauvenet 地影几何 / Chauvenet shadow geometry with an explicit ephemeris instance.
func LunarEclipseShadowGeometryChauvenetAtWithInstance(maximumJDE float64, inst *Instance) LunarEclipseShadowGeometry {
return lunarEclipseShadowGeometryAtWithInstance(maximumJDE, lunarEclipseShadowChauvenet, inst)
}
// LunarEclipseShadowGeometryAtModelWithInstance 指定星历实例及影半径模型的地影几何 / shadow geometry with an explicit instance and shadow-radius model.
func LunarEclipseShadowGeometryAtModelWithInstance(maximumJDE float64, model LunarEclipseShadowModel, inst *Instance) LunarEclipseShadowGeometry {
if model == LunarEclipseShadowModelChauvenet {
return LunarEclipseShadowGeometryChauvenetAtWithInstance(maximumJDE, inst)
}
return LunarEclipseShadowGeometryAtWithInstance(maximumJDE, inst)
}
// LunarEclipseDiagramWithInstance 指定星历实例的 Danjon 月食穿影图 / Danjon lunar eclipse diagram with an explicit ephemeris instance.
func LunarEclipseDiagramWithInstance(seedJDE float64, options LunarEclipseDiagramOptions, inst *Instance) LunarEclipseDiagramResult {
return LunarEclipseDiagramDanjonWithInstance(seedJDE, options, inst)
}
// LunarEclipseDiagramDanjonWithInstance 指定星历实例的 Danjon 月食穿影图 / Danjon lunar eclipse diagram with an explicit ephemeris instance.
func LunarEclipseDiagramDanjonWithInstance(seedJDE float64, options LunarEclipseDiagramOptions, inst *Instance) LunarEclipseDiagramResult {
return lunarEclipseDiagramWithInstance(seedJDE, lunarEclipseShadowDanjon, options, inst)
}
// LunarEclipseDiagramChauvenetWithInstance 指定星历实例的 Chauvenet 月食穿影图 / Chauvenet lunar eclipse diagram with an explicit ephemeris instance.
func LunarEclipseDiagramChauvenetWithInstance(seedJDE float64, options LunarEclipseDiagramOptions, inst *Instance) LunarEclipseDiagramResult {
return lunarEclipseDiagramWithInstance(seedJDE, lunarEclipseShadowChauvenet, options, inst)
}
@@ -0,0 +1,162 @@
package basic
import (
"math"
"reflect"
"testing"
"b612.me/astro/internal/ephem"
"b612.me/astro/internal/geodata"
)
type p3LunarEngine struct {
epoch float64
moonDistance float64
}
func (engine p3LunarEngine) State(target, center ephem.Body, jde float64) ([3]float64, [3]float64, error) {
if center != ephem.Earth || target != ephem.Sun && target != ephem.Moon {
return [3]float64{}, [3]float64{}, ephem.ErrUnsupportedBody
}
longitude, latitude, distance := 0.0, 0.0, astronomicalUnitKM
if target == ephem.Moon {
longitude = math.Pi + 0.23*(jde-engine.epoch)
latitude = 0.002
distance = engine.moonDistance
}
ecliptic := [3]float64{distance * math.Cos(latitude) * math.Cos(longitude), distance * math.Cos(latitude) * math.Sin(longitude), distance * math.Sin(latitude)}
sine, cosine := math.Sincos(Obliquity1980(jde) * math.Pi / 180)
equatorial := [3]float64{ecliptic[0], ecliptic[1]*cosine - ecliptic[2]*sine, ecliptic[1]*sine + ecliptic[2]*cosine}
matrix := ltpPBMAT(2000 + (jde-2451545)/365.25)
var position [3]float64
for axis := range position {
position[axis] = matrix[0][axis]*equatorial[0] + matrix[1][axis]*equatorial[1] + matrix[2][axis]*equatorial[2]
}
return position, [3]float64{}, nil
}
func (engine p3LunarEngine) Coverage(target ephem.Body) (float64, float64, bool) {
return engine.epoch - 20, engine.epoch + 20, target == ephem.Sun || target == ephem.Moon
}
func p3AssertLunarResultEqual(t *testing.T, got, want LunarEclipseResult) {
t.Helper()
gotValue, wantValue := reflect.ValueOf(got), reflect.ValueOf(want)
for field := 0; field < gotValue.NumField(); field++ {
gotField, wantField := gotValue.Field(field), wantValue.Field(field)
if gotField.Kind() == reflect.Float64 && math.IsNaN(gotField.Float()) && math.IsNaN(wantField.Float()) {
continue
}
if !reflect.DeepEqual(gotField.Interface(), wantField.Interface()) {
t.Errorf("%s: got %v, want %v", gotValue.Type().Field(field).Name, gotField.Interface(), wantField.Interface())
}
}
}
func TestP3LunarEclipseBuiltInCompatibility(t *testing.T) {
previous := ephem.Active()
t.Cleanup(func() { ephem.SetActive(previous) })
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
ephem.SetActive(model)
inst := ephem.NewModelInstance(model)
for _, date := range [][3]int{{2022, 11, 8}, {2023, 5, 5}, {2023, 10, 28}, {2024, 4, 23}} {
seed := JDCalc(date[0], date[1], float64(date[2]))
for _, calculate := range []struct {
old func(float64) LunarEclipseResult
new func(float64, *ephem.Instance) LunarEclipseResult
}{
{LunarEclipse, LunarEclipseWithInstance},
{LunarEclipseDanjon, LunarEclipseDanjonWithInstance},
{LunarEclipseChauvenet, LunarEclipseChauvenetWithInstance},
} {
want := calculate.old(seed)
p3AssertLunarResultEqual(t, calculate.new(seed, nil), want)
p3AssertLunarResultEqual(t, calculate.new(seed, inst), want)
}
}
}
}
func TestP3LunarEclipseInstancesStayIsolated(t *testing.T) {
const seed = 2459891.9
first := ephem.NewInstance(p3LunarEngine{epoch: seed, moonDistance: 384400})
second := ephem.NewInstance(p3LunarEngine{epoch: seed + 0.25, moonDistance: 370000})
global, model, generation := ephem.Global(), ephem.Active(), ephem.Generation()
firstResult := LunarEclipseWithInstance(seed, first)
secondResult := LunarEclipseWithInstance(seed, second)
if !firstResult.HasTotal || !secondResult.HasTotal {
t.Fatalf("synthetic eclipses must be total: %+v / %+v", firstResult, secondResult)
}
if math.Abs(secondResult.Maximum-firstResult.Maximum-0.25) > 1e-6 {
t.Errorf("maximum did not follow instance: %.12f / %.12f", firstResult.Maximum, secondResult.Maximum)
}
if firstResult.TotalEnd-firstResult.TotalStart == secondResult.TotalEnd-secondResult.TotalStart {
t.Error("contact durations did not follow instance distances")
}
for _, result := range []LunarEclipseResult{firstResult, secondResult} {
contacts := []float64{result.PenumbralStart, result.PartialStart, result.TotalStart, result.Maximum, result.TotalEnd, result.PartialEnd, result.PenumbralEnd}
for index := 1; index < len(contacts); index++ {
if !isFiniteFloat(contacts[index]) || contacts[index] <= contacts[index-1] {
t.Errorf("invalid contact order: %v", contacts)
}
}
}
p3AssertLunarResultEqual(t, LunarEclipseWithInstance(seed, first), firstResult)
if ephem.Global() != global || ephem.Active() != model || ephem.Generation() != generation {
t.Error("explicit lunar eclipse changed the global ephemeris")
}
}
func TestP3LunarEclipseGeometryAndDiagramUseInstance(t *testing.T) {
const seed = 2459891.9
options := LunarEclipseDiagramOptions{StepDays: 0.02}
for _, shadowModel := range []LunarEclipseShadowModel{LunarEclipseShadowModelDanjon, LunarEclipseShadowModelChauvenet} {
inst := ephem.NewInstance(p3LunarEngine{epoch: seed + 0.15, moonDistance: 380000})
calculate := LunarEclipseDiagramDanjonWithInstance
if shadowModel == LunarEclipseShadowModelChauvenet {
calculate = LunarEclipseDiagramChauvenetWithInstance
}
diagram := calculate(seed, options, inst)
geometry := LunarEclipseShadowGeometryAtModelWithInstance(diagram.Eclipse.Maximum, shadowModel, inst)
if len(diagram.Points) == 0 || math.Abs(geometry.MoonDistanceEarthRadii-380000/geodata.EarthEquatorialRadiusKM) > 1e-10 {
t.Fatalf("diagram/geometry lost instance: %+v", geometry)
}
if math.Abs(diagram.Eclipse.Maximum-seed-0.15) > 0.001 {
t.Errorf("diagram eclipse lost instance: %v", diagram.Eclipse.Maximum)
}
maximumState := computeLunarShadowStateWithInstance(diagram.Eclipse.Maximum, lunarEclipseShadowModel(shadowModel), inst)
for _, point := range diagram.Points {
plane := lunarEclipsePlaneStateAtWithInstance(point.JDE, inst)
if point.X != plane.x/maximumState.moonRadiusRad || point.Y != plane.y/maximumState.moonRadiusRad {
t.Errorf("diagram sample lost instance: %+v", point)
}
}
}
for _, calculate := range []struct {
old func(float64, LunarEclipseDiagramOptions) LunarEclipseDiagramResult
new func(float64, LunarEclipseDiagramOptions, *ephem.Instance) LunarEclipseDiagramResult
}{
{LunarEclipseDiagram, LunarEclipseDiagramWithInstance},
{LunarEclipseDiagramDanjon, LunarEclipseDiagramDanjonWithInstance},
{LunarEclipseDiagramChauvenet, LunarEclipseDiagramChauvenetWithInstance},
} {
got, want := calculate.new(seed, options, nil), calculate.old(seed, options)
p3AssertLunarResultEqual(t, got.Eclipse, want.Eclipse)
got.Eclipse, want.Eclipse = LunarEclipseResult{}, LunarEclipseResult{}
if !reflect.DeepEqual(got, want) {
t.Error("nil changed diagram geometry or samples")
}
}
}
func TestP3LunarEclipseFailureDoesNotFallback(t *testing.T) {
inst := ephem.NewInstance(p3StateEngine{err: ephem.ErrOutOfRange})
result := LunarEclipseWithInstance(2459891.9, inst)
if !math.IsNaN(result.Maximum) || result.HasPenumbral || result.HasPartial || result.HasTotal {
t.Errorf("failed engine produced an eclipse: %+v", result)
}
diagram := LunarEclipseDiagramWithInstance(2459891.9, LunarEclipseDiagramOptions{}, inst)
if !math.IsNaN(diagram.Eclipse.Maximum) || len(diagram.Points) != 0 {
t.Error("failed engine produced diagram samples")
}
}
+17 -6
View File
@@ -1,6 +1,10 @@
package basic
import "math"
import (
"math"
"b612.me/astro/internal/geodata"
)
// LunarEclipseShadowGeometry 是食甚时刻的地影几何。
// 注意单位口径:Gamma 用地球赤道半径,而两个影半径用度——后者是 NASA 月食图上 P./U. Radius 的口径,
@@ -50,11 +54,18 @@ func LunarEclipseShadowGeometryAtModel(maximumJDE float64, model LunarEclipseSha
}
func lunarEclipseShadowGeometryAt(maximumJDE float64, shadowModel lunarEclipseShadowModel) LunarEclipseShadowGeometry {
state := computeLunarShadowState(maximumJDE, shadowModel)
moonDistanceKM := HMoonAway(maximumJDE)
return lunarEclipseShadowGeometryAtWithInstance(maximumJDE, shadowModel, nil)
}
func lunarEclipseShadowGeometryAtWithInstance(maximumJDE float64, shadowModel lunarEclipseShadowModel, inst *Instance) LunarEclipseShadowGeometry {
evaluate := func(jde float64) lunarShadowState {
return computeLunarShadowStateWithInstance(jde, shadowModel, inst)
}
state := evaluate(maximumJDE)
moonDistanceKM := HMoonAwayWithInstance(maximumJDE, inst)
// 影半径是以地心为顶点的角量,除以月球处的地球视差就换成地球赤道半径。
earthParallax := lunarEarthEquatorialRadiusKM / moonDistanceKM
_, _, _, _, minimumDistance := refineLunarEclipseMaximum(maximumJDE, shadowModel)
earthParallax := geodata.EarthEquatorialRadiusKM / moonDistanceKM
_, _, _, _, minimumDistance := refineLunarEclipseMaximumWithEvaluator(maximumJDE, evaluate)
if earthParallax <= 0 {
return LunarEclipseShadowGeometry{}
}
@@ -62,7 +73,7 @@ func lunarEclipseShadowGeometryAt(maximumJDE float64, shadowModel lunarEclipseSh
Gamma: minimumDistance / earthParallax,
PenumbralRadiusDegrees: state.penumbraRadiusRad * 180 / math.Pi,
UmbralRadiusDegrees: state.umbraRadiusRad * 180 / math.Pi,
MoonDistanceEarthRadii: moonDistanceKM / lunarEarthEquatorialRadiusKM,
MoonDistanceEarthRadii: moonDistanceKM / geodata.EarthEquatorialRadiusKM,
// 平面 x 向东(黄经差)、y 向北(黄纬和),方位角自北向东量。
AxisDegrees: minimumDistance * 180 / math.Pi,
}
+147
View File
@@ -0,0 +1,147 @@
package basic
import "math"
// lunarEclipseLimbMaxShiftDays 是月缘精修允许偏离圆月面接触的上限(10 分钟),超出视为不收敛并保留原值。
const lunarEclipseLimbMaxShiftDays = 600.0 / 86400.0
// lunarEclipseLimbStepDays 是割线法的初始步长(1 秒)。
const lunarEclipseLimbStepDays = 1.0 / 86400.0
// LunarEclipseWithLunarLimb 用月缘剖面精修月食接触时刻,食甚、食分与食型仍按圆月面 / refine lunar eclipse contacts with a lunar limb profile.
func LunarEclipseWithLunarLimb(result LunarEclipseResult, limb LunarLimb, inst *Instance) LunarEclipseResult {
if limb == nil {
return result
}
original := result
shadowModel := lunarEclipseShadowDanjon
if result.ShadowModel == LunarEclipseShadowModelChauvenet {
shadowModel = lunarEclipseShadowChauvenet
}
evaluate := func(jde float64) lunarShadowState {
return computeLunarShadowStateWithInstance(jde, shadowModel, inst)
}
refine := func(seed float64, kind lunarEclipseLimbContact) float64 {
if !finite(seed) {
return seed
}
return refineLunarEclipseLimbContact(seed, kind, limb, evaluate, inst)
}
if result.HasPenumbral {
result.PenumbralStart = refine(result.PenumbralStart, lunarEclipseLimbPenumbral)
result.PenumbralEnd = refine(result.PenumbralEnd, lunarEclipseLimbPenumbral)
}
if result.HasPartial {
result.PartialStart = refine(result.PartialStart, lunarEclipseLimbUmbral)
result.PartialEnd = refine(result.PartialEnd, lunarEclipseLimbUmbral)
}
if result.HasTotal {
result.TotalStart = refine(result.TotalStart, lunarEclipseLimbTotal)
result.TotalEnd = refine(result.TotalEnd, lunarEclipseLimbTotal)
}
result.LunarLimbApplied = lunarEclipseLimbChanged(result, original)
return result
}
func lunarEclipseLimbChanged(result, original LunarEclipseResult) bool {
for _, pair := range [][2]float64{
{result.PenumbralStart, original.PenumbralStart},
{result.PenumbralEnd, original.PenumbralEnd},
{result.PartialStart, original.PartialStart},
{result.PartialEnd, original.PartialEnd},
{result.TotalStart, original.TotalStart},
{result.TotalEnd, original.TotalEnd},
} {
if limbTimeChanged(pair[0], pair[1]) {
return true
}
}
return false
}
type lunarEclipseLimbContact int
const (
lunarEclipseLimbPenumbral lunarEclipseLimbContact = iota
lunarEclipseLimbUmbral
lunarEclipseLimbTotal
)
// refineLunarEclipseLimbContact 以圆月面接触为初值用割线法求月缘间隙的零点,不收敛时返回初值。
func refineLunarEclipseLimbContact(seed float64, kind lunarEclipseLimbContact, limb LunarLimb, evaluate lunarShadowEvaluator, inst *Instance) float64 {
t0, t1 := seed, seed+lunarEclipseLimbStepDays
g0, ok0 := lunarEclipseLimbGap(t0, kind, limb, evaluate, inst)
g1, ok1 := lunarEclipseLimbGap(t1, kind, limb, evaluate, inst)
if !ok0 || !ok1 {
return seed
}
for i := 0; i < 12; i++ {
if g1 == g0 {
return seed
}
t2 := t1 - g1*(t1-t0)/(g1-g0)
if !finite(t2) || math.Abs(t2-seed) > lunarEclipseLimbMaxShiftDays {
return seed
}
if math.Abs(t2-t1) < 1e-9 {
return t2
}
g2, ok := lunarEclipseLimbGap(t2, kind, limb, evaluate, inst)
if !ok {
return seed
}
t0, g0, t1, g1 = t1, g1, t2, g2
}
return seed
}
// lunarEclipseLimbGap 是月缘版接触间隙(弧度),零点即接触。
func lunarEclipseLimbGap(jde float64, kind lunarEclipseLimbContact, limb LunarLimb, evaluate lunarShadowEvaluator, inst *Instance) (float64, bool) {
state := evaluate(jde)
moonRA, moonDec := HMoonGeocentricApparentRaDecWithInstance(jde, inst)
sunRA, sunDec := HSunApparentRaDecWithInstance(jde, inst)
moon := lunarEclipseLimbVector(moonRA, moonDec, HMoonAwayWithInstance(jde, inst))
shadow := lunarEclipseLimbVector(sunRA+180, -sunDec, 1)
if !finite(state.moonRadiusRad) || state.moonRadiusRad <= 0 {
return 0, false
}
frame := newLunarLimbFrame(limb, jde, moon, inst)
view, ok := frame.viewToward(moon, shadow)
if !ok {
return 0, false
}
separation := math.Hypot(state.x, state.y)
contact := lunarLimbContact{
frame: &frame,
view: view,
separation: separation,
anglePerKM: state.moonRadiusRad / lunarMoonRadiusScale,
}
switch kind {
case lunarEclipseLimbPenumbral:
contact.targetRadius = state.penumbraRadiusRad
default:
contact.targetRadius = state.umbraRadiusRad
}
// 半影、本影外切取全方向最外的月缘;全食内切取全方向最靠外、最后进入本影的月缘。
var limbRad float64
if kind == lunarEclipseLimbTotal {
limbRad, ok = contact.innerRadius(false)
} else {
limbRad, ok = contact.outerRadius()
}
if !ok {
return 0, false
}
if kind == lunarEclipseLimbTotal {
return separation - math.Abs(limbRad-contact.targetRadius), true
}
return separation - limbRad - contact.targetRadius, true
}
func lunarEclipseLimbVector(raDeg, decDeg, distance float64) [3]float64 {
sinRA, cosRA := math.Sincos(raDeg * rad)
sinDec, cosDec := math.Sincos(decDeg * rad)
return [3]float64{distance * cosDec * cosRA, distance * cosDec * sinRA, distance * sinDec}
}
+12
View File
@@ -3,6 +3,8 @@ package basic
import (
"math"
"testing"
"b612.me/astro/internal/ephemtest"
)
type lunarEclipseBaseline struct {
@@ -117,10 +119,20 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
const timeTolerance = 1e-6
const magnitudeTolerance = 2e-5
values := ephemtest.NewValues(t, "lunar_eclipse_chauvenet")
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := LunarEclipseChauvenet(tc.jde)
w := func(field string, legacy, got float64) float64 { return values.Want(tc.name+"/"+field, legacy, got) }
tc.expectedMax = w("Maximum", tc.expectedMax, result.Maximum)
tc.expectedMag = w("Magnitude", tc.expectedMag, result.Magnitude)
tc.expectedPenumbralStart = w("PenumbralStart", tc.expectedPenumbralStart, result.PenumbralStart)
tc.expectedPenumbralEnd = w("PenumbralEnd", tc.expectedPenumbralEnd, result.PenumbralEnd)
tc.expectedPartialStart = w("PartialStart", tc.expectedPartialStart, result.PartialStart)
tc.expectedPartialEnd = w("PartialEnd", tc.expectedPartialEnd, result.PartialEnd)
tc.expectedTotalStart = w("TotalStart", tc.expectedTotalStart, result.TotalStart)
tc.expectedTotalEnd = w("TotalEnd", tc.expectedTotalEnd, result.TotalEnd)
if result.Type != tc.expectedType {
t.Fatalf("Type mismatch: got %s want %s", result.Type, tc.expectedType)
+424
View File
@@ -0,0 +1,424 @@
package basic
import "math"
// LunarLimb 月缘剖面,nil 为圆月面,内置数据表在 moon/limb / lunar limb profile; nil means a circular limb.
type LunarLimb interface {
// LimbRadiusKM 按观测者总天平动经纬(度,东正北正)与自投影月北极起算的位置角(度)返回月心到月缘距离(km),无值时第二个返回 false / limb radius at a position angle for the given libration.
LimbRadiusKM(librationLongitude, librationLatitude, positionAngle float64) (float64, bool)
}
// lunarLimbFrame 是某一瞬间的月球本体坐标架在真赤道坐标系下的表示,
// 由该时刻的地心天平动与自转轴位置角反解,可对任意站心方向复用。
type lunarLimbFrame struct {
limb LunarLimb
// stepDeg 是剖面位置角网格步长;ranged 为真时 minKM/maxKM 是剖面可能取到的半径范围。
stepDeg float64
minKM, maxKM float64
ranged bool
// axisX / axisY / axisZ 分别是月面经纬 (0°,0°)、(90°,0°) 与北极方向的单位矢量。
axisX [3]float64
axisY [3]float64
axisZ [3]float64
valid bool
}
// newLunarLimbFrame 用地心月球方向与该时刻的物理天平动构造本体坐标架。
// moonGeocentric 是真赤道坐标系下的地心月球矢量(km)。
func newLunarLimbFrame(limb LunarLimb, jdTT float64, moonGeocentric [3]float64, instances ...*Instance) lunarLimbFrame {
if limb == nil {
return lunarLimbFrame{}
}
inst := instanceValue(instances)
toEarth, ok := lunarLimbNormalize([3]float64{-moonGeocentric[0], -moonGeocentric[1], -moonGeocentric[2]})
if !ok {
return lunarLimbFrame{}
}
physical := MoonPhysicalWithInstance(jdTT, inst)
north, east, ok := lunarLimbSkyBasis([3]float64{-toEarth[0], -toEarth[1], -toEarth[2]})
if !ok {
return lunarLimbFrame{}
}
// 月球北极在天空平面上的投影自天北起向东量 P,于是极轴方向 = sinB·(月心→地心) + cosB·N。
sinP, cosP := math.Sincos(physical.PositionAngle * rad)
projected := lunarLimbCombine(cosP, north, sinP, east)
sinB, cosB := math.Sincos(physical.LibrationLatitude * rad)
axisZ, ok := lunarLimbNormalize(lunarLimbCombine(sinB, toEarth, cosB, projected))
if !ok {
return lunarLimbFrame{}
}
// 地心方向在赤道面内的分量对应本体系的 (cosL, sinL, 0),据此旋回 x 轴。
meridian, ok := lunarLimbNormalize(lunarLimbReject(toEarth, axisZ))
if !ok {
return lunarLimbFrame{}
}
sinL, cosL := math.Sincos(physical.LibrationLongitude * rad)
axisX, ok := lunarLimbNormalize(lunarLimbCombine(cosL, meridian, -sinL, lunarLimbCross(axisZ, meridian)))
if !ok {
return lunarLimbFrame{}
}
frame := lunarLimbFrame{
limb: limb,
stepDeg: lunarLimbDefaultStepDeg,
axisX: axisX,
axisY: lunarLimbCross(axisZ, axisX),
axisZ: axisZ,
valid: true,
}
if ranged, ok := limb.(lunarLimbRangeProvider); ok {
low, high := ranged.RadiusRangeKM()
if step := ranged.AngleStepDeg(); finite(step) && step >= lunarLimbMinStepDeg && step <= 1 {
frame.stepDeg = step
}
if finite(low) && finite(high) && low > 0 && high >= low {
frame.minKM, frame.maxKM, frame.ranged = low, high, true
}
}
return frame
}
// radiusKM 返回站心所见月缘在指定方向上的月心距(km);awayFromTarget 为真时取背向目标的一侧。
func (frame lunarLimbFrame) radiusKM(moonTopocentric, targetTopocentric [3]float64, awayFromTarget bool) (float64, bool) {
if !frame.valid {
return 0, false
}
moon, ok := lunarLimbNormalize(moonTopocentric)
if !ok {
return 0, false
}
target, ok := lunarLimbNormalize(targetTopocentric)
if !ok {
return 0, false
}
north, east, ok := lunarLimbSkyBasis(moon)
if !ok {
return 0, false
}
toObserver := [3]float64{-moon[0], -moon[1], -moon[2]}
longitude := math.Atan2(lunarLimbDot(toObserver, frame.axisY), lunarLimbDot(toObserver, frame.axisX)) / rad
latitude := math.Asin(clampUnit(lunarLimbDot(toObserver, frame.axisZ))) / rad
// 自转轴位置角 P 与目标方向位置角 Q 都自天北向东量;剖面表的 θ 自月球北极向月面东经一侧
// (从地球看是天西)量,所以 θ = P − Q。
axisAngle := math.Atan2(lunarLimbDot(frame.axisZ, east), lunarLimbDot(frame.axisZ, north))
targetPlane, ok := lunarLimbNormalize(lunarLimbReject(target, moon))
if !ok {
return 0, false
}
targetAngle := math.Atan2(lunarLimbDot(targetPlane, east), lunarLimbDot(targetPlane, north))
positionAngle := (axisAngle - targetAngle) / rad
if awayFromTarget {
positionAngle += 180
}
positionAngle = math.Mod(positionAngle, 360)
if positionAngle < 0 {
positionAngle += 360
}
radius, ok := frame.limb.LimbRadiusKM(longitude, latitude, positionAngle)
if !ok || !finite(radius) || radius <= 0 {
return 0, false
}
return radius, true
}
// lunarLimbView 是站心所见的月面经纬与指向目标方向的剖面位置角(度)。
type lunarLimbView struct {
longitude, latitude, angle float64
}
// viewToward 求站心所见天平动与目标方向的剖面位置角,约定与 radiusKM 相同。
func (frame lunarLimbFrame) viewToward(moonTopocentric, targetTopocentric [3]float64) (lunarLimbView, bool) {
if !frame.valid {
return lunarLimbView{}, false
}
moon, ok := lunarLimbNormalize(moonTopocentric)
if !ok {
return lunarLimbView{}, false
}
target, ok := lunarLimbNormalize(targetTopocentric)
if !ok {
return lunarLimbView{}, false
}
north, east, ok := lunarLimbSkyBasis(moon)
if !ok {
return lunarLimbView{}, false
}
toObserver := [3]float64{-moon[0], -moon[1], -moon[2]}
axisAngle := math.Atan2(lunarLimbDot(frame.axisZ, east), lunarLimbDot(frame.axisZ, north))
targetPlane, ok := lunarLimbNormalize(lunarLimbReject(target, moon))
if !ok {
return lunarLimbView{}, false
}
targetAngle := math.Atan2(lunarLimbDot(targetPlane, east), lunarLimbDot(targetPlane, north))
return lunarLimbView{
longitude: math.Atan2(lunarLimbDot(toObserver, frame.axisY), lunarLimbDot(toObserver, frame.axisX)) / rad,
latitude: math.Asin(clampUnit(lunarLimbDot(toObserver, frame.axisZ))) / rad,
angle: (axisAngle - targetAngle) / rad,
}, true
}
// radiusAt 返回剖面位置角偏离目标方向 offset 度处的月缘月心距(km)。
func (frame lunarLimbFrame) radiusAt(view lunarLimbView, offset float64) (float64, bool) {
angle := math.Mod(view.angle+offset, 360)
if angle < 0 {
angle += 360
}
radius, ok := frame.limb.LimbRadiusKM(view.longitude, view.latitude, angle)
if !ok || !finite(radius) || radius <= 0 {
return 0, false
}
return radius, true
}
// lunarLimbSkyBasis 返回视方向 direction(观测者指向天体)处的天球北、东单位矢量;
// 东向为赤经增加方向,e = n × direction。
func lunarLimbSkyBasis(direction [3]float64) ([3]float64, [3]float64, bool) {
north, ok := lunarLimbNormalize(lunarLimbReject([3]float64{0, 0, 1}, direction))
if !ok {
return [3]float64{}, [3]float64{}, false
}
return north, lunarLimbCross(north, direction), true
}
func lunarLimbNormalize(vector [3]float64) ([3]float64, bool) {
norm := math.Sqrt(vector[0]*vector[0] + vector[1]*vector[1] + vector[2]*vector[2])
if !finite(norm) || norm <= 0 {
return [3]float64{}, false
}
return [3]float64{vector[0] / norm, vector[1] / norm, vector[2] / norm}, true
}
// lunarLimbReject 返回 vector 中垂直于单位矢量 axis 的分量。
func lunarLimbReject(vector, axis [3]float64) [3]float64 {
projection := lunarLimbDot(vector, axis)
return [3]float64{
vector[0] - projection*axis[0],
vector[1] - projection*axis[1],
vector[2] - projection*axis[2],
}
}
func lunarLimbCombine(scaleA float64, a [3]float64, scaleB float64, b [3]float64) [3]float64 {
return [3]float64{
scaleA*a[0] + scaleB*b[0],
scaleA*a[1] + scaleB*b[1],
scaleA*a[2] + scaleB*b[2],
}
}
func lunarLimbCross(a, b [3]float64) [3]float64 {
return [3]float64{
a[1]*b[2] - a[2]*b[1],
a[2]*b[0] - a[0]*b[2],
a[0]*b[1] - a[1]*b[0],
}
}
func lunarLimbDot(a, b [3]float64) float64 {
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
}
// lunarLimbDefaultStepDeg 剖面不报告网格步长时的位置角采样步长(度)。
const lunarLimbDefaultStepDeg = 0.1
// lunarLimbMinStepDeg 剖面报告的步长低于此值时退回默认步长,防止全圈扫描次数失控。
const lunarLimbMinStepDeg = 0.001
// lunarLimbRangeProvider 可选接口:报告剖面半径范围与位置角网格步长,用于门控与剪枝;步长不在 [0.001°, 1°] 内时改用默认 0.1°。
type lunarLimbRangeProvider interface {
RadiusRangeKM() (float64, float64)
AngleStepDeg() float64
}
// lunarLimbContact 是某一站心时刻按全部位置角求日面接触余量的上下文,角量均为弧度,在天空平面上按平直几何计。
type lunarLimbContact struct {
frame *lunarLimbFrame
view lunarLimbView
distanceKM float64
separation float64
targetRadius float64
// anglePerKM 非零时月缘半径按线性比例换算成角量(月食影面坐标);为零时按 asin(r/distanceKM)。
anglePerKM float64
}
// radius 返回该方向月缘角半径;exact 为假时用 r/距离 的线性近似,只供扫描选方向,极值处再按 exact 重算。
func (contact lunarLimbContact) radius(offsetDeg float64, exact bool) (float64, bool) {
radiusKM, ok := contact.frame.radiusAt(contact.view, offsetDeg)
if !ok {
return 0, false
}
if contact.anglePerKM != 0 {
return radiusKM * contact.anglePerKM, true
}
if !exact {
return radiusKM / contact.distanceKM, true
}
return math.Asin(localSolarEclipseClampUnit(radiusKM / contact.distanceKM)), true
}
// bounds 返回剖面半径的角量上下界;剖面不报告范围时 ok 为假,此时不门控、不剪枝。
func (contact lunarLimbContact) bounds() (float64, float64, bool) {
if !contact.frame.ranged {
return 0, 0, false
}
if contact.anglePerKM != 0 {
return contact.frame.minKM * contact.anglePerKM, contact.frame.maxKM * contact.anglePerKM, true
}
return contact.frame.minKM / contact.distanceKM, contact.frame.maxKM / contact.distanceKM, true
}
// rayChord 返回自月心沿偏离日心方向 δ 的射线与日面边界的近、远交点距离;射线不交日面时 ok 为假。
func (contact lunarLimbContact) rayChord(offsetDeg float64) (float64, float64, bool) {
sine, cosine := math.Sincos(offsetDeg * rad)
d, s := contact.separation, contact.targetRadius
discriminant := s*s - d*d*sine*sine
if discriminant < 0 {
return 0, 0, false
}
root := math.Sqrt(discriminant)
return d*cosine - root, d*cosine + root, true
}
// scan 自 baseDeg 向两侧按剖面位置角网格节点求余量极值并在极值方向按精确半径重算;maximize 为真取最大。剖面在节点间按位置角线性插值,
// 只取节点与 baseDeg 即不会漏掉剖面峰谷。bound(δ) 给出该方向余量可能达到的最好值,越过它不可能再刷新极值即停止该侧。
func (contact lunarLimbContact) scan(
baseDeg float64,
maximize bool,
margin func(offsetDeg float64, exact bool) (float64, bool, bool),
bound func(offsetDeg float64) (float64, bool),
) (float64, bool) {
best, bestOffset, found := 0.0, baseDeg, false
visit := func(offset float64) bool {
value, hit, ok := margin(offset, false)
if !ok {
return false
}
if hit && (!found || (maximize && value > best) || (!maximize && value < best)) {
best, bestOffset, found = value, offset, true
}
return true
}
if !visit(baseDeg) {
return 0, false
}
step := contact.frame.stepDeg
absolute := contact.view.angle + baseDeg
first := [2]float64{math.Floor(absolute/step)*step - absolute, math.Ceil(absolute/step)*step - absolute}
steps := int(math.Ceil(180/step)) + 1
for side, sign := range [2]float64{-1, 1} {
for index := 0; index <= steps; index++ {
delta := first[side] + sign*float64(index)*step
if delta == 0 || math.Abs(delta) > 180 {
continue
}
offset := baseDeg + delta
if limit, ok := bound(offset); ok && found &&
((maximize && limit <= best) || (!maximize && limit >= best)) {
break
}
if !visit(offset) {
return 0, false
}
}
}
if !found {
return 0, false
}
exact, _, ok := margin(bestOffset, true)
return exact, ok
}
// outerRadius 返回外切等效月半径(弧度):取全部方向中月缘超出日面近侧边界最多者,使 d − R − s 为零即外切。
// 只有剖面范围保证间隙离零至少一个起伏幅度时才跳过扫描,免得门控两侧的间隙在接触根附近不连续。
func (contact lunarLimbContact) outerRadius() (float64, bool) {
d, s := contact.separation, contact.targetRadius
low, high, ranged := contact.bounds()
if d <= s || (ranged && (high <= d-s-(high-low) || low >= d-s+(high-low))) {
return contact.radius(0, true)
}
best, ok := contact.scan(0, true,
func(offset float64, exact bool) (float64, bool, bool) {
near, _, hit := contact.rayChord(offset)
if !hit {
return 0, false, true
}
radius, ok := contact.radius(offset, exact)
return radius - near, true, ok
},
func(offset float64) (float64, bool) {
near, _, hit := contact.rayChord(offset)
if !hit {
return math.Inf(-1), true
}
return high - near, ranged
})
if !ok {
return 0, false
}
return d - s + best, true
}
// innerRadius 返回内切等效月半径(弧度),使内切间隙 d − |R − s| 等于 min(−余量, d)。total 为真按全食求月缘盖住日面
// 远侧边界的最小余量,否则按环食求日面远侧边界超出月缘的最小余量;门控条件同 outerRadius。
func (contact lunarLimbContact) innerRadius(total bool) (float64, bool) {
d, s := contact.separation, contact.targetRadius
low, high, ranged := contact.bounds()
relief := high - low
if total {
if ranged && (low >= d+s+relief || high <= d+s-relief) {
radius, ok := contact.radius(0, true)
return s + math.Max(radius-s, 0), ok
}
best, ok := contact.scan(0, false,
func(offset float64, exact bool) (float64, bool, bool) {
_, far, hit := contact.rayChord(offset)
if !hit {
return 0, false, true
}
radius, ok := contact.radius(offset, exact)
return radius - far, true, ok
},
func(offset float64) (float64, bool) {
_, far, hit := contact.rayChord(offset)
if !hit {
return math.Inf(1), true
}
return low - far, ranged
})
if !ok {
return 0, false
}
return s + math.Max(d+best, 0), true
}
// 月心在日面外时环食不可能,内切间隙恰为 d。
if d >= s {
return s, true
}
if ranged && (high <= s-d-relief || low >= s-d+relief) {
radius, ok := contact.radius(180, true)
return math.Min(radius, s), ok
}
best, ok := contact.scan(180, false,
func(offset float64, exact bool) (float64, bool, bool) {
_, far, hit := contact.rayChord(offset)
if !hit {
return 0, false, true
}
radius, ok := contact.radius(offset, exact)
return far - radius, true, ok
},
func(offset float64) (float64, bool) {
_, far, hit := contact.rayChord(offset)
if !hit {
return math.Inf(1), true
}
return far - high, ranged
})
if !ok {
return 0, false
}
return s - math.Max(d+best, 0), true
}
+105
View File
@@ -0,0 +1,105 @@
package basic
// 月缘路径基准:与同一调用的圆月面版本成对,改动扫描步长或剪枝时对照两者倍率。
import (
"testing"
"time"
"b612.me/astro/moon/limb"
)
func benchmarkLocalSolarEclipseLimb(b *testing.B, profile LunarLimb) {
seed := Date2JD(time.Date(2035, 9, 2, 0, 0, 0, 0, time.UTC))
options := SolarEclipseOptions{RadiusModel: SolarEclipseModelNASABulletinSplitK, LunarLimb: profile}
b.ResetTimer()
for index := 0; index < b.N; index++ {
LocalSolarEclipseWithOptionsWithInstance(seed, 116.2310, 39.70, 0, options, nil)
}
}
func BenchmarkLocalSolarEclipseCircular20350902(b *testing.B) { benchmarkLocalSolarEclipseLimb(b, nil) }
func BenchmarkLocalSolarEclipseLunarLimb20350902(b *testing.B) {
benchmarkLocalSolarEclipseLimb(b, limb.Default())
}
func benchmarkStarOccultationLimb(b *testing.B, profile LunarLimb) {
start := time.Date(2026, 2, 10, 12, 0, 0, 0, time.UTC)
options := OccultationSearchOptions{LunarLimb: profile}
b.ResetTimer()
for index := 0; index < b.N; index++ {
if _, err := FindStarOccultationsWithInstance(start, start.Add(36*time.Hour), limbTestAntares, 41.5907, -55.07036, 0, options, nil); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkStarOccultationCircularAntares(b *testing.B) { benchmarkStarOccultationLimb(b, nil) }
func BenchmarkStarOccultationLunarLimbAntares(b *testing.B) {
benchmarkStarOccultationLimb(b, limb.Default())
}
func benchmarkPlanetOccultationLimb(b *testing.B, profile LunarLimb) {
start := time.Date(2024, time.August, 21, 1, 30, 0, 0, time.UTC)
end := time.Date(2024, time.August, 21, 4, 0, 0, 0, time.UTC)
options := OccultationSearchOptions{LunarLimb: profile}
b.ResetTimer()
for index := 0; index < b.N; index++ {
if _, err := FindPlanetOccultationsWithInstance(start, end, OccultationSaturn, -30.072, 43.562592, 0, options, nil); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkPlanetOccultationCircularSaturnGraze(b *testing.B) {
benchmarkPlanetOccultationLimb(b, nil)
}
func BenchmarkPlanetOccultationLunarLimbSaturnGraze(b *testing.B) {
benchmarkPlanetOccultationLimb(b, limb.Default())
}
func BenchmarkStarOccultationLimbProfileAntares(b *testing.B) {
info := StarOccultationInfo{Greatest: time.Date(2026, 2, 11, 3, 47, 15, 0, time.UTC), Observer: Observer{Longitude: 41.5907, Latitude: -55.07036}}
profile := limb.Default()
b.ResetTimer()
for index := 0; index < b.N; index++ {
if _, err := StarOccultationLimbProfileWithInstance(info, limbTestAntares, profile, StarOccultationLimbProfileOptions{}, nil); err != nil {
b.Fatal(err)
}
}
}
func benchmarkSolarEclipseCentralPathLimb(b *testing.B, profile LunarLimb) {
seed := Date2JD(time.Date(2035, 9, 2, 0, 0, 0, 0, time.UTC))
b.ResetTimer()
for index := 0; index < b.N; index++ {
SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{LunarLimb: profile}, nil)
}
}
func BenchmarkSolarEclipseCentralPathCircular20350902(b *testing.B) {
benchmarkSolarEclipseCentralPathLimb(b, nil)
}
func BenchmarkSolarEclipseCentralPathLunarLimb20350902(b *testing.B) {
benchmarkSolarEclipseCentralPathLimb(b, limb.Default())
}
func benchmarkSolarEclipsePartialFootprintsLimb(b *testing.B, profile LunarLimb) {
seed := Date2JD(time.Date(2035, 9, 2, 0, 0, 0, 0, time.UTC))
b.ResetTimer()
for index := 0; index < b.N; index++ {
SolarEclipsePartialFootprintsNASABulletinSplitKWithInstance(seed, SolarEclipsePartialFootprintOptions{LunarLimb: profile}, nil)
}
}
func BenchmarkSolarEclipsePartialFootprintsCircular20350902(b *testing.B) {
benchmarkSolarEclipsePartialFootprintsLimb(b, nil)
}
func BenchmarkSolarEclipsePartialFootprintsLunarLimb20350902(b *testing.B) {
benchmarkSolarEclipsePartialFootprintsLimb(b, limb.Default())
}
+277
View File
@@ -0,0 +1,277 @@
package basic
// 月缘等效半径必须与全部位置角的暴力判据一致,且站心全食与中心带多边形对同一地点结论一致。
import (
"math"
"testing"
"time"
"b612.me/astro/internal/geodata"
"b612.me/astro/moon/limb"
)
func limbBruteMargins(contact lunarLimbContact) (outer, cover, inside float64) {
outer, cover, inside = math.Inf(-1), math.Inf(1), math.Inf(1)
step := contact.frame.stepDeg / 4
start := math.Ceil(contact.view.angle/step)*step - contact.view.angle
for offset := start - 180; offset < 180; offset += step {
radius, ok := contact.radius(offset, true)
if !ok {
continue
}
// 远交点为负的射线背向日面,不是真实交点;月心在日面外时远交点为正即近交点为正。
near, far, hit := contact.rayChord(offset)
if hit && far > 0 {
outer = math.Max(outer, radius-near)
cover = math.Min(cover, radius-far)
}
if _, far, hit := contact.rayChord(offset + 180); hit && far > 0 {
if radius, ok := contact.radius(offset+180, true); ok {
inside = math.Min(inside, far-radius)
}
}
}
return
}
func TestLunarLimbContactMatchesBruteForce(t *testing.T) {
seed := Date2JD(time.Date(2035, 9, 2, 0, 0, 0, 0, time.UTC))
params := solarEclipseModelParams(SolarEclipseModelNASABulletinSplitK, SolarEclipseSunRadiusStandard)
params.lunarLimb = limb.Default()
greatest := localSolarEclipseGreatest(solarEclipseAnchorNewMoon(seed), 116.2310*rad, 39.7215*rad, 0, params)
const tolerance = 1e-4 / 3600 * rad
for _, dtSec := range []float64{-60, -20, -5, 0, 5, 20, 60} {
context := newLocalSolarEclipseStateContext(greatest+dtSec/86400, params)
for _, latDeg := range []float64{39.70, 39.7215, 39.73, 39.74} {
observer := localSolarEclipseObserverXYZ(context.gst, 116.2310*rad, latDeg*rad, 0)
moon := [3]float64{context.moonXYZ[0] - observer[0], context.moonXYZ[1] - observer[1], context.moonXYZ[2] - observer[2]}
sun := [3]float64{context.sunXYZ[0] - observer[0], context.sunXYZ[1] - observer[1], context.sunXYZ[2] - observer[2]}
view, ok := context.limbFrame.viewToward(moon, sun)
if !ok {
t.Fatal("view failed")
}
state := context.stateAt(116.2310*rad, latDeg*rad, 0)
contact := lunarLimbContact{frame: context.limbFrame, view: view, distanceKM: math.Sqrt(lunarLimbDot(moon, moon)),
separation: state.separationRad, targetRadius: state.sunRadiusRad}
outer, cover, _ := limbBruteMargins(contact)
low, high, _ := contact.bounds()
relief := high - low
d, sunRadius := state.separationRad, state.sunRadiusRad
// 门控区只保证间隙符号,门控外须与暴力判据逐值一致。
gotOuter, _ := contact.outerRadius()
wantOuterGap := -outer
if gotGap := d - gotOuter - sunRadius; d > sunRadius && math.Abs(wantOuterGap) < relief {
if math.Abs(gotGap-wantOuterGap) > tolerance {
t.Errorf("dt %.0f lat %.4f: outer gap %.4f\" want %.4f\"", dtSec, latDeg, gotGap/rad*3600, wantOuterGap/rad*3600)
}
} else if (gotGap > 0) != (wantOuterGap > 0) {
t.Errorf("dt %.0f lat %.4f: outer gap sign %.4f\" want %.4f\"", dtSec, latDeg, gotGap/rad*3600, wantOuterGap/rad*3600)
}
gotInner, _ := contact.innerRadius(true)
gotGap := d - math.Abs(gotInner-sunRadius)
wantGap := math.Min(-cover, d)
if math.Abs(cover) < relief {
if math.Abs(gotGap-wantGap) > tolerance {
t.Errorf("dt %.0f lat %.4f: inner gap %.4f\" want %.4f\"", dtSec, latDeg, gotGap/rad*3600, wantGap/rad*3600)
}
} else if (gotGap > 0) != (wantGap > 0) {
t.Errorf("dt %.0f lat %.4f: inner gap sign %.4f\" want %.4f\"", dtSec, latDeg, gotGap/rad*3600, wantGap/rad*3600)
}
}
}
}
func limbPointInBand(segments [][]SolarEclipsePathPoint, lon, lat float64) bool {
inside := false
for _, ring := range segments {
for i, j := 0, len(ring)-1; i < len(ring); j, i = i, i+1 {
a, b := ring[i], ring[j]
if (a.Latitude > lat) != (b.Latitude > lat) &&
lon < (b.Longitude-a.Longitude)*(lat-a.Latitude)/(b.Latitude-a.Latitude)+a.Longitude {
inside = !inside
}
}
}
return inside
}
func TestLimbCentralBandMatchesLocalTotality(t *testing.T) {
seed := Date2JD(time.Date(2035, 9, 2, 0, 0, 0, 0, time.UTC))
profile := limb.Default()
path := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{LunarLimb: profile}, nil)
options := SolarEclipseOptions{RadiusModel: SolarEclipseModelNASABulletinSplitK, LunarLimb: profile}
const lon, marginDeg = 116.2310, 0.5 / 111.2
for lat := 39.60; lat <= 39.85; lat += 0.005 {
inBand := limbPointInBand(path.CentralBandSegments, lon, lat)
if inBand != limbPointInBand(path.CentralBandSegments, lon, lat-marginDeg) ||
inBand != limbPointInBand(path.CentralBandSegments, lon, lat+marginDeg) {
continue
}
local := LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0, options, nil)
if local.HasCentral != inBand {
t.Errorf("lat %.4f: local central %v, band %v", lat, local.HasCentral, inBand)
}
}
local := LocalSolarEclipseWithOptionsWithInstance(seed, lon, 39.7215, 0, options, nil)
if local.HasCentral || limbPointInBand(path.CentralBandSegments, lon, 39.7215) {
t.Errorf("39.7215: local central %v must agree with band (outside)", local.HasCentral)
}
}
// limbTestContact 2035-09-02 北京附近某一时刻的月缘接触上下文,日面半径与角距可替换以构造环食。
func limbTestContact(t *testing.T) lunarLimbContact {
t.Helper()
seed := Date2JD(time.Date(2035, 9, 2, 0, 0, 0, 0, time.UTC))
params := solarEclipseModelParams(SolarEclipseModelNASABulletinSplitK, SolarEclipseSunRadiusStandard)
params.lunarLimb = limb.Default()
greatest := localSolarEclipseGreatest(solarEclipseAnchorNewMoon(seed), 116.2310*rad, 39.7215*rad, 0, params)
context := newLocalSolarEclipseStateContext(greatest, params)
observer := localSolarEclipseObserverXYZ(context.gst, 116.2310*rad, 39.7215*rad, 0)
moon := [3]float64{context.moonXYZ[0] - observer[0], context.moonXYZ[1] - observer[1], context.moonXYZ[2] - observer[2]}
sun := [3]float64{context.sunXYZ[0] - observer[0], context.sunXYZ[1] - observer[1], context.sunXYZ[2] - observer[2]}
view, ok := context.limbFrame.viewToward(moon, sun)
if !ok {
t.Fatal("view failed")
}
state := context.stateAt(116.2310*rad, 39.7215*rad, 0)
return lunarLimbContact{frame: context.limbFrame, view: view, distanceKM: math.Sqrt(lunarLimbDot(moon, moon)),
separation: state.separationRad, targetRadius: state.sunRadiusRad}
}
func TestLunarLimbAnnularAndUnrangedMatchBruteForce(t *testing.T) {
base := limbTestContact(t)
const tolerance = 1e-4 / 3600 * rad
meanRadius := math.Asin(1737.4 / base.distanceKM)
unranged := *base.frame
unranged.ranged = false
for _, ranged := range []bool{true, false} {
for _, separationArcsec := range []float64{0, 1, 5, 12, 20} {
contact := base
if !ranged {
contact.frame = &unranged
}
contact.targetRadius = meanRadius + 6/3600.0*rad
contact.separation = separationArcsec / 3600 * rad
_, _, inside := limbBruteMargins(contact)
got, ok := contact.innerRadius(false)
if !ok {
t.Fatalf("ranged %v d %.0f\": annular radius failed", ranged, separationArcsec)
}
// 环食等效内半径必须小于日面半径,否则食分会大于 1;只比间隙看不出 R 关于 s 的镜像错误。
if got > contact.targetRadius {
t.Errorf("ranged %v d %.0f\": annular radius %.4f\" exceeds the solar radius %.4f\"", ranged, separationArcsec, got/rad*3600, contact.targetRadius/rad*3600)
}
gotGap := contact.separation - math.Abs(got-contact.targetRadius)
wantGap := math.Min(-inside, contact.separation)
if math.Abs(gotGap-wantGap) > tolerance && !(gotGap > 0 && wantGap > 0) {
t.Errorf("ranged %v d %.0f\": annular gap %.4f\" want %.4f\"", ranged, separationArcsec, gotGap/rad*3600, wantGap/rad*3600)
}
if !ranged {
contact.targetRadius = meanRadius - 6/3600.0*rad
contact.separation = (separationArcsec + 1900) / 3600 * rad
outer, _, _ := limbBruteMargins(contact)
got, ok := contact.outerRadius()
if !ok {
t.Fatal("unranged outer failed")
}
if gap := contact.separation - got - contact.targetRadius; math.Abs(gap+outer) > tolerance {
t.Errorf("unranged d %.0f\": outer gap %.4f\" want %.4f\"", separationArcsec, gap/rad*3600, -outer/rad*3600)
}
}
}
}
}
func TestLimbCentralLimitMatchesLocalLimit(t *testing.T) {
seed := Date2JD(time.Date(2035, 9, 2, 0, 0, 0, 0, time.UTC))
profile := limb.Default()
path := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{LunarLimb: profile}, nil)
options := SolarEclipseOptions{RadiusModel: SolarEclipseModelNASABulletinSplitK, LunarLimb: profile}
const lon = 116.2310
bisect := func(inside func(float64) bool) float64 {
low, high := 39.5, 39.9
for i := 0; i < 40; i++ {
mid := (low + high) / 2
if inside(mid) {
high = mid
} else {
low = mid
}
}
return (low + high) / 2
}
local := bisect(func(lat float64) bool {
return LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0, options, nil).HasCentral
})
band := bisect(func(lat float64) bool { return limbPointInBand(path.CentralBandSegments, lon, lat) })
if d := math.Abs(local-band) * 111.2; d > 0.5 {
t.Errorf("southern limit: local %.5f, band %.5f, %.2f km apart", local, band, d)
}
}
func TestLocalSolarEclipseLimbCanFlipTotalToAnnular(t *testing.T) {
seed := Date2JD(time.Date(2023, 4, 20, 0, 0, 0, 0, time.UTC))
const lon, lat = 170.1481, 4.6940
circular := LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0, SolarEclipseOptions{RadiusModel: SolarEclipseModelNASABulletinSplitK}, nil)
if circular.Type != SolarEclipseTotal {
t.Fatalf("circular type %v, want total near the hybrid transition", circular.Type)
}
// 常数月缘比圆月面内半径小 2 km:圆月面判全食,月缘下日面在各方向都露出,应为环食。
innerKM := geodata.EarthEquatorialRadiusKM * solarEclipseUmbralK * localSolarMoonRadiusScale
smaller := &recordingLunarLimb{radiusKM: innerKM - 2}
limbed := LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0,
SolarEclipseOptions{RadiusModel: SolarEclipseModelNASABulletinSplitK, LunarLimb: smaller}, nil)
if limbed.Type != SolarEclipseAnnular || !limbed.HasCentral || !limbed.HasAnnular || limbed.HasTotal {
t.Fatalf("smaller constant limb: type %v central %v annular %v total %v, want annular", limbed.Type, limbed.HasCentral, limbed.HasAnnular, limbed.HasTotal)
}
if limbed.CentralEnd <= limbed.CentralStart {
t.Errorf("annular phase has no duration: %.8f–%.8f", limbed.CentralStart, limbed.CentralEnd)
}
}
// sectorLunarLimb 只在以 center 为中心、半宽 halfWidth 的位置角扇区内给出剖面值,并报告整张表的半径范围。
type sectorLunarLimb struct {
center, halfWidth float64
profile *limb.Profile
}
func (sector sectorLunarLimb) LimbRadiusKM(lon, lat, angle float64) (float64, bool) {
if math.Abs(math.Mod(angle-sector.center+540, 360)-180) > sector.halfWidth {
return 0, false
}
return sector.profile.LimbRadiusKM(lon, lat, angle)
}
func (sector sectorLunarLimb) RadiusRangeKM() (float64, float64) {
return sector.profile.RadiusRangeKM()
}
func (sector sectorLunarLimb) AngleStepDeg() float64 { return sector.profile.AngleStepDeg() }
func TestLocalSolarEclipseStateSeparatesOuterAndInnerLimb(t *testing.T) {
params := solarEclipseModelParams(SolarEclipseModelNASABulletinSplitK, SolarEclipseSunRadiusStandard)
seed := Date2JD(time.Date(2035, 9, 2, 0, 0, 0, 0, time.UTC))
const lon, lat = 116.2310 * rad, 39.70 * rad
// 食甚附近月盘深入日面:外切间隙远离零点被门控,只查日心方向;内切接近零点必须全方向扫描。
tt := localSolarEclipseGreatest(solarEclipseAnchorNewMoon(seed), lon, lat, 0, params)
circular := newLocalSolarEclipseStateContext(tt, params).stateAt(lon, lat, 0)
params.lunarLimb = limb.Default()
full := newLocalSolarEclipseStateContext(tt, params)
observer := localSolarEclipseObserverXYZ(full.gst, lon, lat, 0)
view, ok := full.limbFrame.viewToward(
[3]float64{full.moonXYZ[0] - observer[0], full.moonXYZ[1] - observer[1], full.moonXYZ[2] - observer[2]},
[3]float64{full.sunXYZ[0] - observer[0], full.sunXYZ[1] - observer[1], full.sunXYZ[2] - observer[2]})
if !ok {
t.Fatal("view failed")
}
params.lunarLimb = sectorLunarLimb{center: view.angle, halfWidth: 2, profile: limb.Default()}
state := newLocalSolarEclipseStateContext(tt, params).stateAt(lon, lat, 0)
if !state.outerLimbApplied || state.limbApplied {
t.Fatalf("outerLimbApplied %v limbApplied %v, want outer only", state.outerLimbApplied, state.limbApplied)
}
if state.moonInnerRadiusRad != circular.moonInnerRadiusRad || state.moonOuterRadiusRad == circular.moonOuterRadiusRad {
t.Errorf("inner radius must stay circular and outer must move: inner %.9g vs %.9g, outer %.9g vs %.9g",
state.moonInnerRadiusRad, circular.moonInnerRadiusRad, state.moonOuterRadiusRad, circular.moonOuterRadiusRad)
}
}
+489
View File
@@ -0,0 +1,489 @@
package basic
import (
"math"
"testing"
"time"
"b612.me/astro/internal/geodata"
"b612.me/astro/moon/limb"
)
// recordingLunarLimb 记录最后一次查询参数,返回固定半径。
type recordingLunarLimb struct {
lon, lat, angle float64
radiusKM float64
}
func (limb *recordingLunarLimb) LimbRadiusKM(lon, lat, angle float64) (float64, bool) {
limb.lon, limb.lat, limb.angle = lon, lat, angle
return limb.radiusKM, true
}
func lunarLimbAngleDiff(a, b float64) float64 {
d := math.Mod(a-b, 360)
if d > 180 {
d -= 360
}
if d < -180 {
d += 360
}
return d
}
// 地心观测者所见的月面经纬必须还原该时刻的总天平动;目标位于月球北极投影方向时位置角为 0,
// 向天东转 90° 时为 90°。
func TestLunarLimbFrameRoundTrip(t *testing.T) {
for _, jde := range []float64{2460409.25, 2451545.0, 2462000.7} {
_, moon := solarEclipseSunMoonEquatorial(jde)
moonXYZ := solarEclipseLLRToXYZ(moon[0], moon[1], moon[2])
limb := &recordingLunarLimb{radiusKM: 1737.4}
frame := newLunarLimbFrame(limb, jde, moonXYZ)
if !frame.valid {
t.Fatalf("jde %v: frame invalid", jde)
}
physical := MoonPhysical(jde)
moonUnit, _ := lunarLimbNormalize(moonXYZ)
north, east, _ := lunarLimbSkyBasis(moonUnit)
for _, offset := range []float64{0, 90, 200} {
pa := (physical.PositionAngle + offset) * rad
// 目标放在月心旁 0.5° 处、位置角为 P + offset。
sky := lunarLimbCombine(math.Cos(pa), north, math.Sin(pa), east)
target := lunarLimbCombine(math.Cos(0.5*rad), moonUnit, math.Sin(0.5*rad), sky)
target = [3]float64{target[0] * 1.5e8, target[1] * 1.5e8, target[2] * 1.5e8}
if _, ok := frame.radiusKM(moonXYZ, target, false); !ok {
t.Fatalf("jde %v: radiusKM failed", jde)
}
if d := lunarLimbAngleDiff(limb.lon, physical.LibrationLongitude); math.Abs(d) > 1e-6 {
t.Errorf("jde %v: libration longitude %.9f, want %.9f", jde, limb.lon, physical.LibrationLongitude)
}
if d := limb.lat - physical.LibrationLatitude; math.Abs(d) > 1e-6 {
t.Errorf("jde %v: libration latitude %.9f, want %.9f", jde, limb.lat, physical.LibrationLatitude)
}
if d := lunarLimbAngleDiff(limb.angle, -offset); math.Abs(d) > 1e-6 {
t.Errorf("jde %v offset %v: position angle %.9f", jde, offset, limb.angle)
}
if _, ok := frame.radiusKM(moonXYZ, target, true); !ok {
t.Fatalf("jde %v: away radiusKM failed", jde)
}
if d := lunarLimbAngleDiff(limb.angle, 180-offset); math.Abs(d) > 1e-6 {
t.Errorf("jde %v offset %v: away position angle %.9f", jde, offset, limb.angle)
}
}
}
}
// 参考球半径等于 k·R⊕ 的常数月缘只能让结果在末位附近变化,用来确认替换路径本身不引入偏差。
func TestLocalSolarEclipseConstantLimbMatchesCircular(t *testing.T) {
const seed, lon, lat = 2460409.25, -96.8, 32.8 // 2024-04-08,达拉斯附近
options := SolarEclipseOptions{RadiusModel: SolarEclipseModelIAUSingleK}
circular := LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0, options, nil)
if !circular.HasTotal {
t.Fatalf("expected total eclipse, got %v", circular.Type)
}
options.LunarLimb = &recordingLunarLimb{
radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseIAUSingleRadiusK * localSolarMoonRadiusScale,
}
limbed := LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0, options, nil)
for _, pair := range [][2]float64{
{circular.PartialStart, limbed.PartialStart},
{circular.CentralStart, limbed.CentralStart},
{circular.CentralEnd, limbed.CentralEnd},
{circular.PartialEnd, limbed.PartialEnd},
} {
if d := math.Abs(pair[0]-pair[1]) * 86400; d > 1e-3 {
t.Errorf("contact moved by %.6f s with a constant limb", d)
}
}
}
func TestLunarEclipseConstantLimbMatchesCircular(t *testing.T) {
limb := &recordingLunarLimb{radiusKM: lunarMoonRadiusScale}
for _, chauvenet := range []bool{false, true} {
circular := LunarEclipseDanjonWithInstance(2460926.0, nil)
if chauvenet {
circular = LunarEclipseChauvenetWithInstance(2460926.0, nil)
}
limbed := LunarEclipseWithLunarLimb(circular, limb, nil)
for _, pair := range [][2]float64{
{circular.PenumbralStart, limbed.PenumbralStart},
{circular.PartialStart, limbed.PartialStart},
{circular.TotalStart, limbed.TotalStart},
{circular.TotalEnd, limbed.TotalEnd},
{circular.PartialEnd, limbed.PartialEnd},
{circular.PenumbralEnd, limbed.PenumbralEnd},
} {
if d := math.Abs(pair[0]-pair[1]) * 86400; d > 0.05 {
t.Errorf("chauvenet=%v: contact moved by %.4f s with a constant limb", chauvenet, d)
}
}
if limbed.Maximum != circular.Maximum || limbed.Magnitude != circular.Magnitude {
t.Error("maximum or magnitude changed")
}
}
if got := LunarEclipseWithLunarLimb(LunarEclipseDanjonWithInstance(2460926.0, nil), nil, nil); got != LunarEclipseDanjonWithInstance(2460926.0, nil) {
t.Error("nil limb must return the input unchanged")
}
}
func TestOccultationConstantLimbMatchesCircular(t *testing.T) {
limb := &recordingLunarLimb{radiusKM: moonEquatorialRadiusKM}
start := time.Date(2024, time.August, 21, 1, 30, 0, 0, time.UTC)
end := time.Date(2024, time.August, 21, 4, 0, 0, 0, time.UTC)
observer := Observer{Longitude: -30.072, Latitude: 16.21}
plain, err := FindPlanetOccultationsWithInstance(start, end, OccultationSaturn,
observer.Longitude, observer.Latitude, 0, OccultationSearchOptions{}, nil)
if err != nil || len(plain) != 1 {
t.Fatalf("circular planet search: %v, %d events", err, len(plain))
}
limbed, err := FindPlanetOccultationsWithInstance(start, end, OccultationSaturn,
observer.Longitude, observer.Latitude, 0, OccultationSearchOptions{LunarLimb: limb}, nil)
if err != nil || len(limbed) != 1 {
t.Fatalf("limb planet search: %v, %d events", err, len(limbed))
}
a, b := plain[0], limbed[0]
for i, pair := range [][2]time.Time{
{a.ExternalImmersion, b.ExternalImmersion}, {a.InternalImmersion, b.InternalImmersion},
{a.InternalEmersion, b.InternalEmersion}, {a.ExternalEmersion, b.ExternalEmersion},
} {
if d := math.Abs(pair[0].Sub(pair[1]).Seconds()); d > 0.01 {
t.Errorf("planet contact %d moved by %.4f s with a constant limb", i, d)
}
}
if !a.Greatest.Equal(b.Greatest) || a.Type != b.Type {
t.Error("greatest or type changed")
}
}
func TestGlobalSolarEclipseConstantLimbMatchesCircular(t *testing.T) {
// IAU Single-K 的内外 k 相同,常数月缘可同时复现外切与内切半径。
limb := &recordingLunarLimb{radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseIAUSingleRadiusK * localSolarMoonRadiusScale}
options := SolarEclipseOptions{RadiusModel: SolarEclipseModelIAUSingleK}
for _, seed := range []float64{2460409.0, 2460232.0} {
circular := SolarEclipseWithOptionsWithInstance(seed, options, nil)
withLimb := options
withLimb.LunarLimb = limb
limbed := SolarEclipseWithOptionsWithInstance(seed, withLimb, nil)
for i, pair := range [][2]float64{
{circular.PartialBeginOnEarth, limbed.PartialBeginOnEarth},
{circular.PartialEndOnEarth, limbed.PartialEndOnEarth},
} {
if d := math.Abs(pair[0]-pair[1]) * 86400; d > 0.05 {
t.Errorf("seed %.0f contact %d moved by %.4f s with a constant limb", seed, i, d)
}
}
if limbed.GreatestEclipse != circular.GreatestEclipse || limbed.Type != circular.Type ||
limbed.CentralBeginOnEarth != circular.CentralBeginOnEarth || limbed.PathWidthKM != circular.PathWidthKM {
t.Errorf("seed %.0f: non-contact fields changed", seed)
}
if d := math.Abs(limbed.CentralDurationDays-circular.CentralDurationDays) * 86400; d > 0.05 {
t.Errorf("seed %.0f: central duration moved by %.4f s with a constant limb", seed, d)
}
}
}
func TestCentralPathConstantLimbKeepsLimits(t *testing.T) {
limb := &recordingLunarLimb{radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseUmbralK * localSolarMoonRadiusScale}
for _, seed := range []float64{2460409.0, 2460232.0} {
circular := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{}, nil)
limbed := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{LunarLimb: limb}, nil)
if len(circular.NorthernLimit) == 0 || len(limbed.NorthernLimit) != len(circular.NorthernLimit) ||
len(limbed.SouthernLimit) != len(circular.SouthernLimit) {
t.Fatalf("seed %.0f: limit lengths %d/%d vs %d/%d", seed,
len(limbed.NorthernLimit), len(limbed.SouthernLimit), len(circular.NorthernLimit), len(circular.SouthernLimit))
}
worst := 0.0
for _, pair := range [][2][]SolarEclipsePathPoint{{circular.NorthernLimit, limbed.NorthernLimit}, {circular.SouthernLimit, limbed.SouthernLimit}} {
for i := range pair[0] {
d := math.Hypot((pair[0][i].Longitude-pair[1][i].Longitude)*math.Cos(pair[0][i].Latitude*rad), pair[0][i].Latitude-pair[1][i].Latitude) * 111.2
worst = math.Max(worst, d)
}
}
if worst > 0.05 {
t.Errorf("seed %.0f: constant limb moved a limit by %.3f km", seed, worst)
}
}
}
func TestOccultationPathConstantLimbKeepsLimits(t *testing.T) {
limb := &recordingLunarLimb{radiusKM: moonEquatorialRadiusKM}
star := StarCoordinate{
ID: "HR 4799", RA: 189.1975, Dec: -5.831944444444,
Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), Frame: CoordinateFrameJ2000,
ProperMotionRACosDecMasPerYear: -28, ProperMotionDecMasPerYear: -18,
}
start := time.Date(2025, 6, 4, 16, 0, 0, 0, time.UTC)
end := time.Date(2025, 6, 5, 16, 0, 0, 0, time.UTC)
options := OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 200, DisableFootprints: true, DisableRiseSet: true}
circular, err := FindStarOccultationPathsWithInstance(start, end, star, options, nil)
if err != nil || len(circular) != 1 {
t.Fatalf("circular: %v %d", err, len(circular))
}
options.LunarLimb = limb
limbed, err := FindStarOccultationPathsWithInstance(start, end, star, options, nil)
if err != nil || len(limbed) != 1 {
t.Fatalf("limbed: %v %d", err, len(limbed))
}
worst := 0.0
for _, pair := range [][2][]OccultationPathPoint{{circular[0].NorthernLimit, limbed[0].NorthernLimit}, {circular[0].SouthernLimit, limbed[0].SouthernLimit}} {
if len(pair[0]) == 0 || len(pair[0]) != len(pair[1]) {
t.Fatalf("limit lengths %d vs %d", len(pair[0]), len(pair[1]))
}
for i := range pair[0] {
d := math.Hypot((pair[0][i].Longitude-pair[1][i].Longitude)*math.Cos(pair[0][i].Latitude*rad), pair[0][i].Latitude-pair[1][i].Latitude) * 111.2
worst = math.Max(worst, d)
}
}
if worst > 0.05 {
t.Errorf("constant limb moved a graze limit by %.3f km", worst)
}
if limbed[0].LunarLimbApplied {
t.Fatal("constant circular limb set LunarLimbApplied = true")
}
}
func TestCentralPathConstantLimbKeepsWidthsAndBand(t *testing.T) {
limb := &recordingLunarLimb{radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseUmbralK * localSolarMoonRadiusScale}
for _, seed := range []float64{2460409.0, 2460232.0} {
circular := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{}, nil)
limbed := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{LunarLimb: limb}, nil)
if len(limbed.CenterLine) != len(circular.CenterLine) || len(limbed.CentralBandSegments) != len(circular.CentralBandSegments) {
t.Fatalf("seed %.0f: sample counts changed", seed)
}
worstWidth := 0.0
for i := range circular.CenterLine {
worstWidth = math.Max(worstWidth, math.Abs(limbed.CenterLine[i].WidthKM-circular.CenterLine[i].WidthKM))
}
if worstWidth > 0.1 {
t.Errorf("seed %.0f: constant limb moved a width by %.3f km", seed, worstWidth)
}
if d := math.Abs(limbed.Greatest.WidthKM - circular.Greatest.WidthKM); d > 0.1 {
t.Errorf("seed %.0f: greatest width moved by %.3f km", seed, d)
}
if d := math.Abs(limbed.MaxCentralDurationDays-circular.MaxCentralDurationDays) * 86400; d > 0.05 {
t.Errorf("seed %.0f: max duration moved by %.4f s", seed, d)
}
worstBand := 0.0
for s := range circular.CentralBandSegments {
if len(limbed.CentralBandSegments[s]) != len(circular.CentralBandSegments[s]) {
t.Fatalf("seed %.0f: band segment %d length changed", seed, s)
}
for i, a := range circular.CentralBandSegments[s] {
b := limbed.CentralBandSegments[s][i]
d := math.Hypot((a.Longitude-b.Longitude)*math.Cos(a.Latitude*rad), a.Latitude-b.Latitude) * 111.2
worstBand = math.Max(worstBand, d)
}
}
if worstBand > 0.1 {
t.Errorf("seed %.0f: constant limb moved a band vertex by %.3f km", seed, worstBand)
}
}
}
// failingLunarLimb 处处无值:月缘修正必须整体退回圆月面且不置 LunarLimbApplied。
type failingLunarLimb struct{}
func (failingLunarLimb) LimbRadiusKM(_, _, _ float64) (float64, bool) { return 0, false }
func TestFailingLimbKeepsEclipsesCircular(t *testing.T) {
const seed, lon, lat = 2460409.25, -96.8, 32.8 // 2024-04-08,达拉斯附近
limb := failingLunarLimb{}
for _, model := range []SolarEclipseRadiusModel{SolarEclipseModelNASABulletinSplitK, SolarEclipseModelIAUSingleK} {
options := SolarEclipseOptions{RadiusModel: model}
withLimb := options
withLimb.LunarLimb = limb
circularLocal := LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0, options, nil)
limbedLocal := LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0, withLimb, nil)
if limbedLocal != circularLocal {
t.Errorf("model %v: failing limb changed the local result", model)
}
circularGlobal := SolarEclipseWithOptionsWithInstance(seed, options, nil)
limbedGlobal := SolarEclipseWithOptionsWithInstance(seed, withLimb, nil)
if limbedGlobal.LunarLimbApplied ||
limbedGlobal.PartialBeginOnEarth != circularGlobal.PartialBeginOnEarth ||
limbedGlobal.PartialEndOnEarth != circularGlobal.PartialEndOnEarth ||
limbedGlobal.CentralDurationDays != circularGlobal.CentralDurationDays {
t.Errorf("model %v: failing limb changed the global result or set LunarLimbApplied", model)
}
}
circularLunar := LunarEclipseDanjonWithInstance(2460926.0, nil)
if got := LunarEclipseWithLunarLimb(circularLunar, limb, nil); got != circularLunar {
t.Error("failing limb changed the lunar eclipse result")
}
}
func TestPartialBoundaryLimbCorrection(t *testing.T) {
const seed = 2460409.25 // 2024-04-08
circularOptions := SolarEclipsePartialFootprintOptions{}
circular := solarEclipsePartialFootprints(seed, SolarEclipseModelIAUSingleK, circularOptions, nil)
if len(circular.PartialBandContours) == 0 || len(circular.RiseSetCurves) == 0 {
t.Fatal("circular partial boundary missing")
}
samePoints := func(a, b []SolarEclipsePathPoint, toleranceKM float64) float64 {
worst := 0.0
for index := range a {
km := math.Hypot(signedAngleDifference(a[index].Longitude, b[index].Longitude)*111.19*math.Cos(a[index].Latitude*rad),
(a[index].Latitude-b[index].Latitude)*111.19)
worst = math.Max(worst, km)
}
return worst
}
compare := func(name string, limb LunarLimb, wantApplied bool, maxShiftKM float64) {
options := circularOptions
options.LunarLimb = limb
limbed := solarEclipsePartialFootprints(seed, SolarEclipseModelIAUSingleK, options, nil)
if limbed.PartialBoundaryLimbApplied != wantApplied {
t.Errorf("%s: PartialBoundaryLimbApplied = %v, want %v", name, limbed.PartialBoundaryLimbApplied, wantApplied)
}
if limbed.RiseSetTopologyDegraded {
t.Errorf("%s: rise/set topology degraded", name)
}
if len(limbed.PartialBandContours) != len(circular.PartialBandContours) || len(limbed.RiseSetCurves) != len(circular.RiseSetCurves) {
t.Fatalf("%s: boundary structure changed", name)
}
worst := 0.0
for index := range circular.PartialBandContours {
worst = math.Max(worst, samePoints(circular.PartialBandContours[index], limbed.PartialBandContours[index], 0))
}
for index, curve := range circular.RiseSetCurves {
for segment := range curve.Segments {
shift := samePoints(curve.Segments[segment], limbed.RiseSetCurves[index].Segments[segment], 0)
if curve.Phase == RiseSetPhaseGreatest && shift != 0 {
t.Errorf("%s: greatest rise/set curve moved by %.3f km", name, shift)
}
worst = math.Max(worst, shift)
}
}
if worst > maxShiftKM {
t.Errorf("%s: boundary moved by %.3f km, want <= %.3f km", name, worst, maxShiftKM)
}
if wantApplied && worst < 0.5 {
t.Errorf("%s: boundary moved only %.3f km", name, worst)
}
}
compare("failing", failingLunarLimb{}, false, 0)
compare("constant", &recordingLunarLimb{
radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseIAUSingleRadiusK * localSolarMoonRadiusScale,
}, false, 0.05)
compare("profile", sawtoothLunarLimb{}, true, limbLimitMaxShiftKM)
}
// sawtoothLunarLimb 以位置角 10° 为周期在参考半径上叠加 ±3 km 起伏,模拟月缘山谷。
type sawtoothLunarLimb struct{}
func (sawtoothLunarLimb) LimbRadiusKM(_, _, angle float64) (float64, bool) {
phase := math.Mod(angle, 10) / 10
return geodata.EarthEquatorialRadiusKM*solarEclipseIAUSingleRadiusK*localSolarMoonRadiusScale + 6*math.Abs(phase-0.5) - 1.5, true
}
// TestGlobalSolarEclipseLimbContactMatchesTime 检查月缘修正后的偏食始末接触点与修正时刻同源:
// 接触点必须落在该时刻重解的半影外切点上,而不是沿地球边缘扫描网格上的样点。
func TestGlobalSolarEclipseLimbContactMatchesTime(t *testing.T) {
for _, seed := range []float64{2460409.25, 2460232.25, 2461275.25} {
options := SolarEclipseOptions{RadiusModel: SolarEclipseModelNASABulletinSplitK, LunarLimb: limb.Default()}
result := SolarEclipseWithOptionsWithInstance(seed, options, nil)
if !result.LunarLimbApplied {
t.Fatalf("seed %.2f: the default limb profile changed nothing", seed)
}
solver := newSolarEclipseSolverWithOptions(CalcMoonSHByJDE(seed, 0), options, nil)
for _, pair := range []struct {
name string
jde float64
contact SolarEclipseLimbContact
}{
{"P1", result.PartialBeginOnEarth, result.PartialBeginContact},
{"P4", result.PartialEndOnEarth, result.PartialEndContact},
} {
if !pair.contact.Defined {
t.Errorf("seed %.2f %s: the corrected contact location is missing", seed, pair.name)
continue
}
point, ok := solver.shadowContactPointAt(pair.jde, solarEclipsePenumbralShadow, false)
if !ok {
t.Errorf("seed %.2f %s: no penumbral tangent point at the corrected time", seed, pair.name)
continue
}
longitude := signedAngleDifference(pair.contact.Longitude, point.Longitude) * math.Cos(point.Latitude*rad)
if d := math.Hypot(longitude, pair.contact.Latitude-point.Latitude) * geodata.EarthKMPerDegree; d > 0.05 {
t.Errorf("seed %.2f %s: contact location is %.3f km from the tangent point at its own time", seed, pair.name, d)
}
}
}
}
// TestLimbFlagsIgnoreNoiseWithConstantLimb 检查常数月缘等价于圆月面时,各层月缘标志都不因离散残差误报。
func TestLimbFlagsIgnoreNoiseWithConstantLimb(t *testing.T) {
iau := &recordingLunarLimb{radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseIAUSingleRadiusK * localSolarMoonRadiusScale}
umbral := &recordingLunarLimb{radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseUmbralK * localSolarMoonRadiusScale}
for _, seed := range []float64{2460409.25, 2460232.25, 2461275.25} {
global := SolarEclipseWithOptionsWithInstance(seed, SolarEclipseOptions{RadiusModel: SolarEclipseModelIAUSingleK, LunarLimb: iau}, nil)
if global.LunarLimbApplied || global.PartialBeginContact.Defined || global.PartialEndContact.Defined {
t.Errorf("seed %.2f: constant limb reported a global correction", seed)
}
central := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{LunarLimb: umbral}, nil)
if central.CentralLimitsLimbApplied {
t.Errorf("seed %.2f: constant limb reported moved central limits", seed)
}
local := LocalSolarEclipseWithOptionsWithInstance(seed, -96.8, 32.8, 0, SolarEclipseOptions{RadiusModel: SolarEclipseModelIAUSingleK, LunarLimb: iau}, nil)
if local.LunarLimbApplied {
t.Errorf("seed %.2f: constant limb reported a local correction", seed)
}
}
real := SolarEclipseCentralPathNASABulletinSplitKWithInstance(2460409.25, SolarEclipsePathOptions{LunarLimb: limb.Default()}, nil)
if !real.CentralLimitsLimbApplied {
t.Error("default limb profile must move the central limits")
}
}
// TestLocalSolarEclipseLimbFlagCoversMagnitude 检查只改变食分、遮蔽率而不改变接触时刻时站心标志仍为真。
func TestLocalSolarEclipseLimbFlagCoversMagnitude(t *testing.T) {
circular := LocalSolarEclipseResult{Type: SolarEclipsePartial, PartialStart: 1, PartialEnd: 2, Magnitude: 0.5, Obscuration: 0.4}
limbed := circular
if localSolarEclipseLimbChanged(limbed, circular) {
t.Fatal("identical results reported a change")
}
limbed.Magnitude += 1e-12
if localSolarEclipseLimbChanged(limbed, circular) {
t.Error("floating-point noise in magnitude reported a change")
}
limbed.Magnitude = circular.Magnitude + 1e-6
if !localSolarEclipseLimbChanged(limbed, circular) {
t.Error("a visible magnitude change was not reported")
}
limbed = circular
limbed.Obscuration += 1e-6
if !localSolarEclipseLimbChanged(limbed, circular) {
t.Error("a visible obscuration change was not reported")
}
limbed = circular
limbed.Type = SolarEclipseAnnular
if !localSolarEclipseLimbChanged(limbed, circular) {
t.Error("a type change was not reported")
}
}
// steppedLunarLimb 报告指定网格步长的常半径剖面。
type steppedLunarLimb struct{ step float64 }
func (limb steppedLunarLimb) LimbRadiusKM(lon, lat, angle float64) (float64, bool) {
return 1737.4, true
}
func (limb steppedLunarLimb) RadiusRangeKM() (float64, float64) { return 1737.4, 1737.4 }
func (limb steppedLunarLimb) AngleStepDeg() float64 { return limb.step }
func TestLunarLimbFrameRejectsTinyStep(t *testing.T) {
_, moon := solarEclipseSunMoonEquatorial(2460409.25)
moonXYZ := solarEclipseLLRToXYZ(moon[0], moon[1], moon[2])
for _, c := range []struct{ step, want float64 }{
{1e-300, lunarLimbDefaultStepDeg}, {1e-4, lunarLimbDefaultStepDeg}, {0.001, 0.001}, {0.5, 0.5}, {2, lunarLimbDefaultStepDeg},
} {
frame := newLunarLimbFrame(steppedLunarLimb{c.step}, 2460409.25, moonXYZ)
if !frame.valid || frame.stepDeg != c.want {
t.Errorf("step %g: got %g valid=%v, want %g", c.step, frame.stepDeg, frame.valid, c.want)
}
}
}
+27 -44
View File
@@ -125,59 +125,34 @@ func MarsMag(jde float64) float64 {
return FloatRound(mag, 2)
}
func MarsHeight(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := MarsApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(hourAngle)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(hourAngle)
return ArcSin(sinHeight)
// MarsHeightWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MarsHeightWithInstance(localJD, lon, lat, timezone float64, inst *Instance) float64 {
return planetHeightN(3, localJD, lon, lat, timezone, -1, inst)
}
func MarsAzimuth(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := MarsApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 三角转换公式
tanAzimuth := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(dec)*Cos(lat))
azimuth := ArcTan(tanAzimuth)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
}
return azimuth + 180
}
if hourAngle/15 < 12 {
return azimuth + 180
}
return azimuth
// MarsAzimuthWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MarsAzimuthWithInstance(localJD, lon, lat, timezone float64, inst *Instance) float64 {
return planetAzimuthN(3, localJD, lon, lat, timezone, -1, inst)
}
func MarsHourAngle(jd, lon, timezone float64) float64 {
// MarsHourAngleWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MarsHourAngleWithInstance(jd, lon, timezone float64, inst *Instance) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - MarsApparentRa(UTC2TT(jd-timezone/24.0))
hourAngle := siderealLongitude - MarsApparentRaWithInstance(UTC2TT(jd-timezone/24.0), inst)
if hourAngle < 0 {
hourAngle += 360
}
return hourAngle
}
func MarsCulminationTime(localJD, lon, timezone float64) float64 {
// MarsCulminationTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MarsCulminationTimeWithInstance(localJD, lon, timezone float64, inst *Instance) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-MarsHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
estimateJD := localJD + Limit360(360-MarsHourAngleWithInstance(localJD, lon, timezone, inst))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := MarsHourAngle(localJD, lon, timezone)
currentHourAngle := MarsHourAngleWithInstance(localJD, lon, timezone, inst)
if currentHourAngle < 180 {
currentHourAngle += 360
}
@@ -195,14 +170,22 @@ func MarsCulminationTime(localJD, lon, timezone float64) float64 {
return estimateJD
}
func MarsRiseTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return marsRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true)
// MarsRiseTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MarsRiseTimeWithInstance(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, inst *Instance) (float64, error) {
return marsRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true, inst)
}
func MarsSetTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return marsRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false)
// MarsSetTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MarsSetTimeWithInstance(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, inst *Instance) (float64, error) {
return marsRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false, inst)
}
func marsRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool) (float64, error) {
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, MarsCulminationTime, MarsHeight, MarsApparentDec)
func marsRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool, instances ...*Instance) (float64, error) {
inst := instanceValue(instances)
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, func(localJD, lon, timezone float64) float64 {
return MarsCulminationTimeWithInstance(localJD, lon, timezone, inst)
}, func(localJD, lon, lat, timezone float64) float64 {
return MarsHeightWithInstance(localJD, lon, lat, timezone, inst)
}, func(jde float64) float64 { return MarsApparentDecWithInstance(jde, inst) })
}
+4 -4
View File
@@ -3,7 +3,6 @@ package basic
import (
"encoding/json"
"math"
"os"
"testing"
)
@@ -20,10 +19,11 @@ type marsEventBaseline struct {
func loadMarsEventBaseline(t *testing.T) marsEventBaseline {
t.Helper()
data, err := os.ReadFile("testdata/mars_event_baseline.json")
if err != nil {
t.Fatal(err)
var named []namedEventFunc
for _, c := range marsEventCases() {
named = append(named, namedEventFunc{c.name, c.fn})
}
data := eventBaselineData(t, "testdata/mars_event_baseline.json", named)
var baseline marsEventBaseline
if err := json.Unmarshal(data, &baseline); err != nil {
+101 -59
View File
@@ -17,8 +17,10 @@ const (
marsStationHalfWindowDay = 6.0
)
func marsSunLongitudeDelta(jde, degree float64, filter bool) float64 {
sub := Limit360(Limit360(MarsApparentLo(jde)-HSunApparentLo(jde)) - degree)
func marsSunLongitudeDelta(jde, degree float64, filter bool, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := Limit360(Limit360(MarsApparentLoWithInstance(jde, inst)-HSunApparentLoWithInstance(jde, inst)) - degree)
if filter {
if sub > 180 {
sub -= 360
@@ -30,8 +32,10 @@ func marsSunLongitudeDelta(jde, degree float64, filter bool) float64 {
return sub
}
func marsSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64 {
sub := Limit360(Limit360(MarsApparentLoN(jde, n)-HSunApparentLoN(jde, n)) - degree)
func marsSunLongitudeDeltaN(jde, degree float64, filter bool, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := Limit360(Limit360(MarsApparentLoNWithInstance(jde, n, inst)-HSunApparentLoNWithInstance(jde, n, inst)) - degree)
if filter {
if sub > 180 {
sub -= 360
@@ -43,8 +47,10 @@ func marsSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64 {
return sub
}
func marsRADerivative(jde, val float64) float64 {
sub := MarsApparentRa(jde+val) - MarsApparentRa(jde-val)
func marsRADerivative(jde, val float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := MarsApparentRaWithInstance(jde+val, inst) - MarsApparentRaWithInstance(jde-val, inst)
if sub > 180 {
sub -= 360
}
@@ -54,8 +60,10 @@ func marsRADerivative(jde, val float64) float64 {
return sub / (2 * val)
}
func marsRADerivativeN(jde, val float64, n int) float64 {
sub := MarsApparentRaN(jde+val, n) - MarsApparentRaN(jde-val, n)
func marsRADerivativeN(jde, val float64, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := MarsApparentRaNWithInstance(jde+val, n, inst) - MarsApparentRaNWithInstance(jde-val, n, inst)
if sub > 180 {
sub -= 360
}
@@ -65,13 +73,15 @@ func marsRADerivativeN(jde, val float64, n int) float64 {
return sub / (2 * val)
}
func marsConjunctionFull(jde, degree float64, next uint8) float64 {
func marsConjunctionFull(jde, degree float64, next uint8, instances ...*Instance) float64 {
inst := instanceValue(instances)
//0=last 1=next
if !isFiniteFloat(jde) || !isFiniteFloat(degree) {
return math.NaN()
}
daysPerDegree := MARS_S_PERIOD / 360
currentDelta := marsSunLongitudeDelta(jde, degree, false)
currentDelta := marsSunLongitudeDelta(jde, degree, false, inst)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
@@ -81,8 +91,8 @@ func marsConjunctionFull(jde, degree float64, next uint8) float64 {
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJDE+0.000005, degree, true) - marsSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
longitudeDelta := marsSunLongitudeDelta(prevJDE, degree, true, inst)
longitudeSlope := (marsSunLongitudeDelta(prevJDE+0.000005, degree, true, inst) - marsSunLongitudeDelta(prevJDE-0.000005, degree, true, inst)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
@@ -96,13 +106,15 @@ func marsConjunctionFull(jde, degree float64, next uint8) float64 {
return TT2UTC(estimateJDE)
}
func marsConjunction(jde, degree float64, next uint8) float64 {
func marsConjunction(jde, degree float64, next uint8, instances ...*Instance) float64 {
inst := instanceValue(instances)
//0=last 1=next
if !isFiniteFloat(jde) || !isFiniteFloat(degree) {
return math.NaN()
}
daysPerDegree := MARS_S_PERIOD / 360
currentDelta := marsSunLongitudeDelta(jde, degree, false)
currentDelta := marsSunLongitudeDelta(jde, degree, false, inst)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
@@ -112,8 +124,8 @@ func marsConjunction(jde, degree float64, next uint8) float64 {
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDeltaN(prevJDE, degree, true, marsEventSearchN)
longitudeSlope := (marsSunLongitudeDeltaN(prevJDE+0.000005, degree, true, marsEventSearchN) - marsSunLongitudeDeltaN(prevJDE-0.000005, degree, true, marsEventSearchN)) / 0.00001
longitudeDelta := marsSunLongitudeDeltaN(prevJDE, degree, true, marsEventSearchN, inst)
longitudeSlope := (marsSunLongitudeDeltaN(prevJDE+0.000005, degree, true, marsEventSearchN, inst) - marsSunLongitudeDeltaN(prevJDE-0.000005, degree, true, marsEventSearchN, inst)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= marsPhaseCoarseTolerance {
@@ -127,8 +139,8 @@ func marsConjunction(jde, degree float64, next uint8) float64 {
converged = false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJDE+0.000005, degree, true) - marsSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
longitudeDelta := marsSunLongitudeDelta(prevJDE, degree, true, inst)
longitudeSlope := (marsSunLongitudeDelta(prevJDE+0.000005, degree, true, inst) - marsSunLongitudeDelta(prevJDE-0.000005, degree, true, inst)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
@@ -142,39 +154,65 @@ func marsConjunction(jde, degree float64, next uint8) float64 {
return TT2UTC(estimateJDE)
}
func LastMarsConjunction(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 0, marsConjunction)
// LastMarsConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMarsConjunctionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 0, func(jde, degree float64, next uint8) float64 {
return marsConjunction(jde, degree, next, inst)
})
}
func NextMarsConjunction(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 0, marsConjunction)
// NextMarsConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMarsConjunctionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 0, func(jde, degree float64, next uint8) float64 {
return marsConjunction(jde, degree, next, inst)
})
}
func LastMarsOpposition(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 180, marsConjunction)
// LastMarsOppositionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMarsOppositionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 180, func(jde, degree float64, next uint8) float64 {
return marsConjunction(jde, degree, next, inst)
})
}
func NextMarsOpposition(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 180, marsConjunction)
// NextMarsOppositionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMarsOppositionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 180, func(jde, degree float64, next uint8) float64 {
return marsConjunction(jde, degree, next, inst)
})
}
func NextMarsEasternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 90, marsConjunction)
// NextMarsEasternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMarsEasternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 90, func(jde, degree float64, next uint8) float64 {
return marsConjunction(jde, degree, next, inst)
})
}
func LastMarsEasternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 90, marsConjunction)
// LastMarsEasternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMarsEasternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 90, func(jde, degree float64, next uint8) float64 {
return marsConjunction(jde, degree, next, inst)
})
}
func NextMarsWesternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 270, marsConjunction)
// NextMarsWesternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMarsWesternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 270, func(jde, degree float64, next uint8) float64 {
return marsConjunction(jde, degree, next, inst)
})
}
func LastMarsWesternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 270, marsConjunction)
// LastMarsWesternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMarsWesternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 270, func(jde, degree float64, next uint8) float64 {
return marsConjunction(jde, degree, next, inst)
})
}
func marsRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
func marsRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool, instances ...*Instance) float64 {
inst := instanceValue(instances)
if !isFiniteFloat(oppositionJD) {
return math.NaN()
}
@@ -182,103 +220,107 @@ func marsRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
easternQuadratureUT := marsConjunction(oppositionTT, 90, 0)
easternQuadratureUT := marsConjunction(oppositionTT, 90, 0, inst)
startTT = UTC2TT(easternQuadratureUT)
} else {
westernQuadratureUT := marsConjunction(oppositionTT, 270, 1)
westernQuadratureUT := marsConjunction(oppositionTT, 270, 1, inst)
endTT = UTC2TT(westernQuadratureUT)
}
bestJDE := zeroEventInWindow(startTT, endTT, marsStationCoarseStepDay, marsStationHalfWindowDay, 30.0/86400.0, func(jd float64) float64 {
return marsRADerivativeN(jd, marsStationDerivativeStepDay, marsEventSearchN)
return marsRADerivativeN(jd, marsStationDerivativeStepDay, marsEventSearchN, inst)
}, func(jd float64) float64 {
return marsRADerivative(jd, marsStationDerivativeStepDay)
return marsRADerivative(jd, marsStationDerivativeStepDay, inst)
})
return TT2UTC(bestJDE)
}
func NextMarsRetrogradeToPrograde(jde float64) float64 {
// NextMarsRetrogradeToProgradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMarsRetrogradeToProgradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
lastOppositionJD := marsConjunctionFull(jde, 180, 0, inst)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false, inst)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
nextOppositionJD := marsConjunctionFull(jde, 180, 1, inst)
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
date = marsRetrogradeAroundOpposition(nextOppositionJD, false)
date = marsRetrogradeAroundOpposition(nextOppositionJD, false, inst)
if !isFiniteFloat(date) || !stationUTQueryAfterOrEqual(date, jde) {
return math.NaN()
}
return date
}
func LastMarsRetrogradeToPrograde(jde float64) float64 {
// LastMarsRetrogradeToProgradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMarsRetrogradeToProgradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
lastOppositionJD := marsConjunctionFull(jde, 180, 0, inst)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false, inst)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
if !isFiniteFloat(lastOppositionJD) {
return math.NaN()
}
previousOppositionJD := marsConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
previousOppositionJD := marsConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0, inst)
if !isFiniteFloat(previousOppositionJD) {
return math.NaN()
}
date = marsRetrogradeAroundOpposition(previousOppositionJD, false)
date = marsRetrogradeAroundOpposition(previousOppositionJD, false, inst)
if !isFiniteFloat(date) || !stationUTQueryBeforeOrEqual(date, jde) {
return math.NaN()
}
return date
}
func NextMarsProgradeToRetrograde(jde float64) float64 {
// NextMarsProgradeToRetrogradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMarsProgradeToRetrogradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
nextOppositionJD := marsConjunctionFull(jde, 180, 1, inst)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true, inst)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
followingOppositionJD := marsConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
followingOppositionJD := marsConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1, inst)
if !isFiniteFloat(followingOppositionJD) {
return math.NaN()
}
date = marsRetrogradeAroundOpposition(followingOppositionJD, true)
date = marsRetrogradeAroundOpposition(followingOppositionJD, true, inst)
if !isFiniteFloat(date) || !stationUTQueryAfterOrEqual(date, jde) {
return math.NaN()
}
return date
}
func LastMarsProgradeToRetrograde(jde float64) float64 {
// LastMarsProgradeToRetrogradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMarsProgradeToRetrogradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
nextOppositionJD := marsConjunctionFull(jde, 180, 1, inst)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true, inst)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
lastOppositionJD := marsConjunctionFull(jde, 180, 0, inst)
if !isFiniteFloat(lastOppositionJD) {
return math.NaN()
}
date = marsRetrogradeAroundOpposition(lastOppositionJD, true)
date = marsRetrogradeAroundOpposition(lastOppositionJD, true, inst)
if !isFiniteFloat(date) || !stationUTQueryBeforeOrEqual(date, jde) {
return math.NaN()
}
+27 -44
View File
@@ -108,59 +108,34 @@ func MercuryMag(jde float64) float64 {
return FloatRound(mag, 2)
}
func MercuryHeight(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := MercuryApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(hourAngle)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(hourAngle)
return ArcSin(sinHeight)
// MercuryHeightWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MercuryHeightWithInstance(localJD, lon, lat, timezone float64, inst *Instance) float64 {
return planetHeightN(1, localJD, lon, lat, timezone, -1, inst)
}
func MercuryAzimuth(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := MercuryApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 三角转换公式
tanAzimuth := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(dec)*Cos(lat))
azimuth := ArcTan(tanAzimuth)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
}
return azimuth + 180
}
if hourAngle/15 < 12 {
return azimuth + 180
}
return azimuth
// MercuryAzimuthWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MercuryAzimuthWithInstance(localJD, lon, lat, timezone float64, inst *Instance) float64 {
return planetAzimuthN(1, localJD, lon, lat, timezone, -1, inst)
}
func MercuryHourAngle(jd, lon, timezone float64) float64 {
// MercuryHourAngleWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MercuryHourAngleWithInstance(jd, lon, timezone float64, inst *Instance) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - MercuryApparentRa(UTC2TT(jd-timezone/24.0))
hourAngle := siderealLongitude - MercuryApparentRaWithInstance(UTC2TT(jd-timezone/24.0), inst)
if hourAngle < 0 {
hourAngle += 360
}
return hourAngle
}
func MercuryCulminationTime(localJD, lon, timezone float64) float64 {
// MercuryCulminationTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MercuryCulminationTimeWithInstance(localJD, lon, timezone float64, inst *Instance) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-MercuryHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
estimateJD := localJD + Limit360(360-MercuryHourAngleWithInstance(localJD, lon, timezone, inst))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := MercuryHourAngle(localJD, lon, timezone)
currentHourAngle := MercuryHourAngleWithInstance(localJD, lon, timezone, inst)
if currentHourAngle < 180 {
currentHourAngle += 360
}
@@ -178,14 +153,22 @@ func MercuryCulminationTime(localJD, lon, timezone float64) float64 {
return estimateJD
}
func MercuryRiseTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return mercuryRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true)
// MercuryRiseTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MercuryRiseTimeWithInstance(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, inst *Instance) (float64, error) {
return mercuryRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true, inst)
}
func MercurySetTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return mercuryRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false)
// MercurySetTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MercurySetTimeWithInstance(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, inst *Instance) (float64, error) {
return mercuryRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false, inst)
}
func mercuryRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool) (float64, error) {
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, MercuryCulminationTime, MercuryHeight, MercuryApparentDec)
func mercuryRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool, instances ...*Instance) (float64, error) {
inst := instanceValue(instances)
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, func(localJD, lon, timezone float64) float64 {
return MercuryCulminationTimeWithInstance(localJD, lon, timezone, inst)
}, func(localJD, lon, lat, timezone float64) float64 {
return MercuryHeightWithInstance(localJD, lon, lat, timezone, inst)
}, func(jde float64) float64 { return MercuryApparentDecWithInstance(jde, inst) })
}
+4 -4
View File
@@ -3,7 +3,6 @@ package basic
import (
"encoding/json"
"math"
"os"
"testing"
)
@@ -20,10 +19,11 @@ type mercuryEventBaseline struct {
func loadMercuryEventBaseline(t *testing.T) mercuryEventBaseline {
t.Helper()
data, err := os.ReadFile("testdata/mercury_event_baseline.json")
if err != nil {
t.Fatal(err)
var named []namedEventFunc
for _, c := range mercuryEventCases() {
named = append(named, namedEventFunc{c.name, c.fn})
}
data := eventBaselineData(t, "testdata/mercury_event_baseline.json", named)
var baseline mercuryEventBaseline
if err := json.Unmarshal(data, &baseline); err != nil {
+306 -220
View File
@@ -3,7 +3,6 @@ package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
@@ -51,11 +50,13 @@ type mercuryConjunctionResult struct {
geoLightDays float64
}
func mercuryHelioN(planetIndex int, jde float64, n int) mercuryConjunctionLBR {
func mercuryHelioN(planetIndex int, jde float64, n int, instances ...*Instance) mercuryConjunctionLBR {
inst := instanceValue(instances)
return mercuryConjunctionLBR{
lo: planet.WherePlanetN(planetIndex, 0, jde, n),
bo: planet.WherePlanetN(planetIndex, 1, jde, n),
r: planet.WherePlanetN(planetIndex, 2, jde, n),
lo: PlanetCoordinateNWithInstance(planetIndex, 0, jde, n, inst),
bo: PlanetCoordinateNWithInstance(planetIndex, 1, jde, n, inst),
r: PlanetCoordinateNWithInstance(planetIndex, 2, jde, n, inst),
}
}
@@ -82,17 +83,21 @@ func mercuryConjunctionAngleDelta(diff float64) float64 {
return diff
}
func mercuryConjunctionHeliocentricDelta(jde, targetDeg float64, n int) float64 {
planetLo := planet.WherePlanetN(1, 0, jde, n)
earthLo := planet.WherePlanetN(-1, 0, jde, n)
func mercuryConjunctionHeliocentricDelta(jde, targetDeg float64, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
planetLo := PlanetCoordinateNWithInstance(1, 0, jde, n, inst)
earthLo := PlanetCoordinateNWithInstance(-1, 0, jde, n, inst)
return mercuryConjunctionAngleDelta(planetLo - earthLo - targetDeg)
}
func mercuryConjunctionDifference(jd float64, n int, targetDeg, sunLightDays, geoLightDays float64) mercuryConjunctionResult {
earthForSun := mercuryHelioN(-1, jd-sunLightDays, n)
func mercuryConjunctionDifference(jd float64, n int, targetDeg, sunLightDays, geoLightDays float64, instances ...*Instance) mercuryConjunctionResult {
inst := instanceValue(instances)
earthForSun := mercuryHelioN(-1, jd-sunLightDays, n, inst)
sunLo := Limit360(earthForSun.lo + 180)
earth := mercuryHelioN(-1, jd-geoLightDays, n)
planetPos := mercuryHelioN(1, jd-geoLightDays, n)
earth := mercuryHelioN(-1, jd-geoLightDays, n, inst)
planetPos := mercuryHelioN(1, jd-geoLightDays, n, inst)
geo := mercuryGeocentric(planetPos, earth)
return mercuryConjunctionResult{
diff: mercuryConjunctionAngleDelta(geo.lo - sunLo - targetDeg),
@@ -101,38 +106,44 @@ func mercuryConjunctionDifference(jd float64, n int, targetDeg, sunLightDays, ge
}
}
func mercuryConjunctionExactDelta(jde float64) float64 {
return mercuryConjunctionAngleDelta(MercuryApparentLo(jde) - HSunApparentLo(jde))
func mercuryConjunctionExactDelta(jde float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
return mercuryConjunctionAngleDelta(MercuryApparentLoWithInstance(jde, inst) - HSunApparentLoWithInstance(jde, inst))
}
func mercuryConjunctionApproxTT(seed float64, inferior bool) float64 {
func mercuryConjunctionApproxTT(seed float64, inferior bool, instances ...*Instance) float64 {
inst := instanceValue(instances)
heliocentricTarget := 180.0
if inferior {
heliocentricTarget = 0
}
jde := seed
for i := 0; i < 6; i++ {
jde -= mercuryConjunctionHeliocentricDelta(jde, heliocentricTarget, 8) / (360.0 / MERCURY_S_PERIOD)
jde -= mercuryConjunctionHeliocentricDelta(jde, heliocentricTarget, 8, inst) / (360.0 / MERCURY_S_PERIOD)
}
startSample := mercuryConjunctionDifference(jde, 8, 0, 0, 0)
nextSample := mercuryConjunctionDifference(jde+mercuryConjunctionDerivativeStepDay, 8, 0, 0, 0)
startSample := mercuryConjunctionDifference(jde, 8, 0, 0, 0, inst)
nextSample := mercuryConjunctionDifference(jde+mercuryConjunctionDerivativeStepDay, 8, 0, 0, 0, inst)
diffSlope := mercuryConjunctionAngleDelta(nextSample.diff-startSample.diff) / mercuryConjunctionDerivativeStepDay
refined := mercuryConjunctionDifference(jde, 40, 0, startSample.sunLightDays, startSample.geoLightDays)
refined := mercuryConjunctionDifference(jde, 40, 0, startSample.sunLightDays, startSample.geoLightDays, inst)
jde -= refined.diff / diffSlope
final := mercuryConjunctionDifference(jde, -1, 0, refined.sunLightDays, refined.geoLightDays)
final := mercuryConjunctionDifference(jde, -1, 0, refined.sunLightDays, refined.geoLightDays, inst)
jde -= final.diff / diffSlope
return jde
}
func mercuryConjunctionExactTT(seed float64, inferior bool) float64 {
estimateJD := mercuryConjunctionApproxTT(seed, inferior)
func mercuryConjunctionExactTT(seed float64, inferior bool, instances ...*Instance) float64 {
inst := instanceValue(instances)
estimateJD := mercuryConjunctionApproxTT(seed, inferior, inst)
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJD
longitudeDelta := mercuryConjunctionExactDelta(prevJDE)
longitudeSlope := (mercuryConjunctionExactDelta(prevJDE+0.000005) - mercuryConjunctionExactDelta(prevJDE-0.000005)) / 0.00001
longitudeDelta := mercuryConjunctionExactDelta(prevJDE, inst)
longitudeSlope := (mercuryConjunctionExactDelta(prevJDE+0.000005, inst) - mercuryConjunctionExactDelta(prevJDE-0.000005, inst)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
@@ -153,15 +164,17 @@ func mercuryConjunctionExactTT(seed float64, inferior bool) float64 {
// 并且下游的类型化「留」会因此返回错事件、甚至让 Last 返回未来。
// 新实现改成方向性括号扫描:截断级数逐段找异号区间(顺序扫描 ⇒ 一定取最近的一个合),
// 再用全项级数在括号内抛光;扫满 640 天仍无括号时有界返回 NaN。
func mercuryConjunction(jde float64, next uint8) float64 {
func mercuryConjunction(jde float64, next uint8, instances ...*Instance) float64 {
inst := instanceValue(instances)
//0=last 1=next
if !isFiniteFloat(jde) {
return math.NaN()
}
if math.Abs(mercuryConjunctionExactDelta(jde)) <= mercuryConjunctionSameInstantDegrees {
if math.Abs(mercuryConjunctionExactDelta(jde, inst)) <= mercuryConjunctionSameInstantDegrees {
best := math.NaN()
consider := func(inferior bool) {
eventUT := TT2UTC(mercuryConjunctionExactTT(jde, inferior))
eventUT := TT2UTC(mercuryConjunctionExactTT(jde, inferior, inst))
if !isFiniteFloat(eventUT) {
return
}
@@ -187,18 +200,18 @@ func mercuryConjunction(jde float64, next uint8) float64 {
direction = -1
}
leftJDE := jde
leftValue := mercuryConjunctionDeltaN(leftJDE, mercuryEventSearchN)
leftValue := mercuryConjunctionDeltaN(leftJDE, mercuryEventSearchN, inst)
if !isFiniteFloat(leftValue) {
return math.NaN()
}
for i := 0; i < mercuryConjunctionScanMaxSteps; i++ {
rightJDE := jde + direction*mercuryConjunctionScanStepDay*float64(i+1)
rightValue := mercuryConjunctionDeltaN(rightJDE, mercuryEventSearchN)
rightValue := mercuryConjunctionDeltaN(rightJDE, mercuryEventSearchN, inst)
if !isFiniteFloat(rightValue) {
return math.NaN()
}
if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 {
return mercuryConjunctionPolish(jde, leftJDE, rightJDE, direction)
return mercuryConjunctionPolish(jde, leftJDE, rightJDE, direction, inst)
}
leftJDE, leftValue = rightJDE, rightValue
}
@@ -206,24 +219,28 @@ func mercuryConjunction(jde float64, next uint8) float64 {
}
// mercuryConjunctionDeltaN 截断级数下的水星-太阳视黄经差(度,[-180,180]),用于方向性括号扫描。
func mercuryConjunctionDeltaN(jde float64, n int) float64 {
return mercuryConjunctionAngleDelta(MercuryApparentLoN(jde, n) - HSunApparentLoN(jde, n))
func mercuryConjunctionDeltaN(jde float64, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
return mercuryConjunctionAngleDelta(MercuryApparentLoNWithInstance(jde, n, inst) - HSunApparentLoNWithInstance(jde, n, inst))
}
// mercuryConjunctionPolish 用全项级数在截断级数给出的括号内抛光。
// 截断误差可能让括号两端在全项函数上同号(罕见),此时沿扫描方向再扩一两个扫描步;
// 若仍未被确认(典型情形:查询几乎正好落在合上,截断级数在根两侧的符号与全项不一致),
// 退回全项级数的方向扫描,保证有界且不返回 NaN。
func mercuryConjunctionPolish(jde, leftJDE, rightJDE, direction float64) float64 {
func mercuryConjunctionPolish(jde, leftJDE, rightJDE, direction float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
for attempt := 0; attempt < 3; attempt++ {
leftValue := mercuryConjunctionExactDelta(leftJDE)
rightValue := mercuryConjunctionExactDelta(rightJDE)
leftValue := mercuryConjunctionExactDelta(leftJDE, inst)
rightValue := mercuryConjunctionExactDelta(rightJDE, inst)
if !isFiniteFloat(leftValue) || !isFiniteFloat(rightValue) {
return math.NaN()
}
if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 {
root, ok := eventBracketSecantRoot(leftJDE, rightJDE, leftValue, rightValue,
mercuryConjunctionPolishToleranceDay, mercuryConjunctionExactDelta)
mercuryConjunctionPolishToleranceDay, func(jde float64) float64 { return mercuryConjunctionExactDelta(jde, inst) })
if !ok {
return math.NaN()
}
@@ -235,25 +252,27 @@ func mercuryConjunctionPolish(jde, leftJDE, rightJDE, direction float64) float64
}
leftJDE -= mercuryConjunctionScanStepDay
}
return mercuryConjunctionFullDirectionalScan(jde, direction)
return mercuryConjunctionFullDirectionalScan(jde, direction, inst)
}
// mercuryConjunctionFullDirectionalScan 全项级数的方向扫描(截断括号未被确认时的兜底)。
func mercuryConjunctionFullDirectionalScan(jde, direction float64) float64 {
func mercuryConjunctionFullDirectionalScan(jde, direction float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
leftJDE := jde
leftValue := mercuryConjunctionExactDelta(leftJDE)
leftValue := mercuryConjunctionExactDelta(leftJDE, inst)
if !isFiniteFloat(leftValue) {
return math.NaN()
}
for i := 0; i < mercuryConjunctionScanMaxSteps; i++ {
rightJDE := jde + direction*mercuryConjunctionScanStepDay*float64(i+1)
rightValue := mercuryConjunctionExactDelta(rightJDE)
rightValue := mercuryConjunctionExactDelta(rightJDE, inst)
if !isFiniteFloat(rightValue) {
return math.NaN()
}
if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 {
root, ok := eventBracketSecantRoot(leftJDE, rightJDE, leftValue, rightValue,
mercuryConjunctionPolishToleranceDay, mercuryConjunctionExactDelta)
mercuryConjunctionPolishToleranceDay, func(jde float64) float64 { return mercuryConjunctionExactDelta(jde, inst) })
if !ok {
return math.NaN()
}
@@ -264,56 +283,66 @@ func mercuryConjunctionFullDirectionalScan(jde, direction float64) float64 {
return math.NaN()
}
func LastMercuryConjunction(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryConjunctionStrict, NextMercuryConjunctionStrict)
// LastMercuryConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryConjunctionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastSimpleEvent(jde, func(jde float64) float64 { return LastMercuryConjunctionStrictWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryConjunctionStrictWithInstance(jde, inst) })
}
func NextMercuryConjunction(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryConjunctionStrict, NextMercuryConjunctionStrict)
// NextMercuryConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryConjunctionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextSimpleEvent(jde, func(jde float64) float64 { return LastMercuryConjunctionStrictWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryConjunctionStrictWithInstance(jde, inst) })
}
func LastMercuryConjunctionStrict(jde float64) float64 {
return mercuryConjunction(jde, 0)
// LastMercuryConjunctionStrictWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryConjunctionStrictWithInstance(jde float64, inst *Instance) float64 {
return mercuryConjunction(jde, 0, inst)
}
func NextMercuryConjunctionStrict(jde float64) float64 {
return mercuryConjunction(jde, 1)
// NextMercuryConjunctionStrictWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryConjunctionStrictWithInstance(jde float64, inst *Instance) float64 {
return mercuryConjunction(jde, 1, inst)
}
func NextMercuryInferiorConjunction(jde float64) float64 {
date := NextMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) > EarthAway(date) {
return NextMercuryConjunctionStrict(date + 2)
// NextMercuryInferiorConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryInferiorConjunctionWithInstance(jde float64, inst *Instance) float64 {
date := NextMercuryConjunctionStrictWithInstance(jde, inst)
if EarthMercuryAwayWithInstance(date, inst) > EarthAwayWithInstance(date, inst) {
return NextMercuryConjunctionStrictWithInstance(date+2, inst)
}
return date
}
func NextMercurySuperiorConjunction(jde float64) float64 {
date := NextMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) < EarthAway(date) {
return NextMercuryConjunctionStrict(date + 2)
// NextMercurySuperiorConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercurySuperiorConjunctionWithInstance(jde float64, inst *Instance) float64 {
date := NextMercuryConjunctionStrictWithInstance(jde, inst)
if EarthMercuryAwayWithInstance(date, inst) < EarthAwayWithInstance(date, inst) {
return NextMercuryConjunctionStrictWithInstance(date+2, inst)
}
return date
}
func LastMercuryInferiorConjunction(jde float64) float64 {
date := LastMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) > EarthAway(date) {
return LastMercuryConjunctionStrict(date - 2)
// LastMercuryInferiorConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryInferiorConjunctionWithInstance(jde float64, inst *Instance) float64 {
date := LastMercuryConjunctionStrictWithInstance(jde, inst)
if EarthMercuryAwayWithInstance(date, inst) > EarthAwayWithInstance(date, inst) {
return LastMercuryConjunctionStrictWithInstance(date-2, inst)
}
return date
}
func LastMercurySuperiorConjunction(jde float64) float64 {
date := LastMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) < EarthAway(date) {
return LastMercuryConjunctionStrict(date - 2)
// LastMercurySuperiorConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercurySuperiorConjunctionWithInstance(jde float64, inst *Instance) float64 {
date := LastMercuryConjunctionStrictWithInstance(jde, inst)
if EarthMercuryAwayWithInstance(date, inst) < EarthAwayWithInstance(date, inst) {
return LastMercuryConjunctionStrictWithInstance(date-2, inst)
}
return date
}
func mercuryRADerivative(jde, delta float64) float64 {
sub := MercuryApparentRa(jde+delta) - MercuryApparentRa(jde-delta)
func mercuryRADerivative(jde, delta float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := MercuryApparentRaWithInstance(jde+delta, inst) - MercuryApparentRaWithInstance(jde-delta, inst)
if sub > 180 {
sub -= 360
}
@@ -323,8 +352,10 @@ func mercuryRADerivative(jde, delta float64) float64 {
return sub / (2 * delta)
}
func mercuryRADerivativeN(jde, delta float64, n int) float64 {
sub := MercuryApparentRaN(jde+delta, n) - MercuryApparentRaN(jde-delta, n)
func mercuryRADerivativeN(jde, delta float64, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := MercuryApparentRaNWithInstance(jde+delta, n, inst) - MercuryApparentRaNWithInstance(jde-delta, n, inst)
if sub > 180 {
sub -= 360
}
@@ -334,16 +365,20 @@ func mercuryRADerivativeN(jde, delta float64, n int) float64 {
return sub / (2 * delta)
}
func mercuryStationInWindow(startTT, endTT float64) float64 {
func mercuryStationInWindow(startTT, endTT float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
bestJDE := zeroEventInWindow(startTT, endTT, mercuryStationCoarseStepDay, mercuryStationHalfWindowDay, 30.0/86400.0, func(jd float64) float64 {
return mercuryRADerivativeN(jd, mercuryStationDerivativeStepDay, mercuryEventSearchN)
return mercuryRADerivativeN(jd, mercuryStationDerivativeStepDay, mercuryEventSearchN, inst)
}, func(jd float64) float64 {
return mercuryRADerivative(jd, mercuryStationDerivativeStepDay)
return mercuryRADerivative(jd, mercuryStationDerivativeStepDay, inst)
})
return TT2UTC(bestJDE)
}
func mercuryStationBetween(startTT, endTT float64) bool {
func mercuryStationBetween(startTT, endTT float64, instances ...*Instance) bool {
inst := instanceValue(instances)
if endTT < startTT {
startTT, endTT = endTT, startTT
}
@@ -360,13 +395,13 @@ func mercuryStationBetween(startTT, endTT float64) bool {
}
// 截断扫描足以判断单候选快速路径是否安全 / A truncated scan is enough to decide whether the one-candidate fast path is safe.
left := startTT
leftValue := mercuryRADerivativeN(left, mercuryStationDerivativeStepDay, mercuryEventSearchN)
leftValue := mercuryRADerivativeN(left, mercuryStationDerivativeStepDay, mercuryEventSearchN, inst)
for left < endTT {
right := left + mercuryStationCoarseStepDay
if right > endTT {
right = endTT
}
rightValue := mercuryRADerivativeN(right, mercuryStationDerivativeStepDay, mercuryEventSearchN)
rightValue := mercuryRADerivativeN(right, mercuryStationDerivativeStepDay, mercuryEventSearchN, inst)
if leftValue == 0 || leftValue*rightValue < 0 || rightValue == 0 {
return true
}
@@ -376,76 +411,81 @@ func mercuryStationBetween(startTT, endTT float64) bool {
return false
}
func mercuryProgradeToRetrogradeAroundInferior(inferiorUT float64) float64 {
func mercuryProgradeToRetrogradeAroundInferior(inferiorUT float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
inferiorTT := UTC2TT(inferiorUT)
return mercuryStationInWindow(inferiorTT-mercuryStationWindowDays, inferiorTT)
return mercuryStationInWindow(inferiorTT-mercuryStationWindowDays, inferiorTT, inst)
}
func mercuryRetrogradeToProgradeAroundInferior(inferiorUT float64) float64 {
func mercuryRetrogradeToProgradeAroundInferior(inferiorUT float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
inferiorTT := UTC2TT(inferiorUT)
return mercuryStationInWindow(inferiorTT, inferiorTT+mercuryStationWindowDays)
return mercuryStationInWindow(inferiorTT, inferiorTT+mercuryStationWindowDays, inst)
}
// 类型化「留」统一结构:以下合为锚点求候选,再用侧向不变量(Next 不得早于查询、
// Last 不得晚于查询)校验;不满足则把锚点推进/回退一个会合周期重试,最多
// mercuryStationAnchorAttempts 次,最终仍有界失败为 NaN。
// 这保证 Last* 永远不会返回未来、Next* 永远不会返回过去的事件。
func NextMercuryProgradeToRetrograde(jde float64) float64 {
// NextMercuryProgradeToRetrogradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryProgradeToRetrogradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
inferior := NextMercuryInferiorConjunction(jde)
// 以下合为锚点求候选,不满足 Next 不早于查询时推进一个会合周期,保证不返回过去的事件。
inferior := NextMercuryInferiorConjunctionWithInstance(jde, inst)
for i := 0; i < mercuryStationAnchorAttempts && isFiniteFloat(inferior); i++ {
date := mercuryProgradeToRetrogradeAroundInferior(inferior)
date := mercuryProgradeToRetrogradeAroundInferior(inferior, inst)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
inferior = NextMercuryInferiorConjunction(eventUTNextQueryTT(inferior))
inferior = NextMercuryInferiorConjunctionWithInstance(eventUTNextQueryTT(inferior), inst)
}
return math.NaN()
}
func NextMercuryRetrogradeToPrograde(jde float64) float64 {
// NextMercuryRetrogradeToProgradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryRetrogradeToProgradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
inferior := LastMercuryInferiorConjunction(jde)
inferior := LastMercuryInferiorConjunctionWithInstance(jde, inst)
for i := 0; i < mercuryStationAnchorAttempts && isFiniteFloat(inferior); i++ {
date := mercuryRetrogradeToProgradeAroundInferior(inferior)
date := mercuryRetrogradeToProgradeAroundInferior(inferior, inst)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
inferior = NextMercuryInferiorConjunction(eventUTNextQueryTT(inferior))
inferior = NextMercuryInferiorConjunctionWithInstance(eventUTNextQueryTT(inferior), inst)
}
return math.NaN()
}
func LastMercuryProgradeToRetrograde(jde float64) float64 {
// LastMercuryProgradeToRetrogradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryProgradeToRetrogradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
inferior := NextMercuryInferiorConjunction(jde)
inferior := NextMercuryInferiorConjunctionWithInstance(jde, inst)
for i := 0; i < mercuryStationAnchorAttempts && isFiniteFloat(inferior); i++ {
date := mercuryProgradeToRetrogradeAroundInferior(inferior)
date := mercuryProgradeToRetrogradeAroundInferior(inferior, inst)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
inferior = LastMercuryInferiorConjunction(eventUTLastQueryTT(inferior))
inferior = LastMercuryInferiorConjunctionWithInstance(eventUTLastQueryTT(inferior), inst)
}
return math.NaN()
}
func LastMercuryRetrogradeToPrograde(jde float64) float64 {
// LastMercuryRetrogradeToProgradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryRetrogradeToProgradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
inferior := LastMercuryInferiorConjunction(jde)
inferior := LastMercuryInferiorConjunctionWithInstance(jde, inst)
for i := 0; i < mercuryStationAnchorAttempts && isFiniteFloat(inferior); i++ {
date := mercuryRetrogradeToProgradeAroundInferior(inferior)
date := mercuryRetrogradeToProgradeAroundInferior(inferior, inst)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
inferior = LastMercuryInferiorConjunction(eventUTLastQueryTT(inferior))
inferior = LastMercuryInferiorConjunctionWithInstance(eventUTLastQueryTT(inferior), inst)
}
return math.NaN()
}
@@ -477,72 +517,78 @@ func latestFiniteEventUT(candidates ...float64) float64 {
return best
}
func nextMercuryRetrogradeFromTyped(jde float64) float64 {
return earliestFiniteEventUT(NextMercuryProgradeToRetrograde(jde), NextMercuryRetrogradeToPrograde(jde))
func nextMercuryRetrogradeFromTyped(jde float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
return earliestFiniteEventUT(NextMercuryProgradeToRetrogradeWithInstance(jde, inst), NextMercuryRetrogradeToProgradeWithInstance(jde, inst))
}
func NextMercuryRetrograde(jde float64) float64 {
// NextMercuryRetrogradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryRetrogradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay)
motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay, inst)
if motion > mercuryStationMotionTolerance {
p2r := NextMercuryProgradeToRetrograde(jde)
if isFiniteFloat(p2r) && !mercuryStationBetween(jde, UTC2TT(p2r)) {
p2r := NextMercuryProgradeToRetrogradeWithInstance(jde, inst)
if isFiniteFloat(p2r) && !mercuryStationBetween(jde, UTC2TT(p2r), inst) {
return p2r
}
best := earliestFiniteEventUT(p2r, NextMercuryRetrogradeToPrograde(jde))
best := earliestFiniteEventUT(p2r, NextMercuryRetrogradeToProgradeWithInstance(jde, inst))
if !stationUTQueryAfterOrEqual(best, jde) {
return math.NaN()
}
return best
}
if motion < -mercuryStationMotionTolerance {
r2p := NextMercuryRetrogradeToPrograde(jde)
if isFiniteFloat(r2p) && !mercuryStationBetween(jde, UTC2TT(r2p)) {
r2p := NextMercuryRetrogradeToProgradeWithInstance(jde, inst)
if isFiniteFloat(r2p) && !mercuryStationBetween(jde, UTC2TT(r2p), inst) {
return r2p
}
best := earliestFiniteEventUT(NextMercuryProgradeToRetrograde(jde), r2p)
best := earliestFiniteEventUT(NextMercuryProgradeToRetrogradeWithInstance(jde, inst), r2p)
if !stationUTQueryAfterOrEqual(best, jde) {
return math.NaN()
}
return best
}
return nextMercuryRetrogradeFromTyped(jde)
return nextMercuryRetrogradeFromTyped(jde, inst)
}
func lastMercuryRetrogradeFromTyped(jde float64) float64 {
return latestFiniteEventUT(LastMercuryProgradeToRetrograde(jde), LastMercuryRetrogradeToPrograde(jde))
func lastMercuryRetrogradeFromTyped(jde float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
return latestFiniteEventUT(LastMercuryProgradeToRetrogradeWithInstance(jde, inst), LastMercuryRetrogradeToProgradeWithInstance(jde, inst))
}
func LastMercuryRetrograde(jde float64) float64 {
// LastMercuryRetrogradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryRetrogradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay)
motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay, inst)
if motion > mercuryStationMotionTolerance {
r2p := LastMercuryRetrogradeToPrograde(jde)
if isFiniteFloat(r2p) && !mercuryStationBetween(UTC2TT(r2p), jde) {
r2p := LastMercuryRetrogradeToProgradeWithInstance(jde, inst)
if isFiniteFloat(r2p) && !mercuryStationBetween(UTC2TT(r2p), jde, inst) {
return r2p
}
best := latestFiniteEventUT(LastMercuryProgradeToRetrograde(jde), r2p)
best := latestFiniteEventUT(LastMercuryProgradeToRetrogradeWithInstance(jde, inst), r2p)
if !stationUTQueryBeforeOrEqual(best, jde) {
return math.NaN()
}
return best
}
if motion < -mercuryStationMotionTolerance {
p2r := LastMercuryProgradeToRetrograde(jde)
if isFiniteFloat(p2r) && !mercuryStationBetween(UTC2TT(p2r), jde) {
p2r := LastMercuryProgradeToRetrogradeWithInstance(jde, inst)
if isFiniteFloat(p2r) && !mercuryStationBetween(UTC2TT(p2r), jde, inst) {
return p2r
}
best := latestFiniteEventUT(p2r, LastMercuryRetrogradeToPrograde(jde))
best := latestFiniteEventUT(p2r, LastMercuryRetrogradeToProgradeWithInstance(jde, inst))
if !stationUTQueryBeforeOrEqual(best, jde) {
return math.NaN()
}
return best
}
return lastMercuryRetrogradeFromTyped(jde)
return lastMercuryRetrogradeFromTyped(jde, inst)
}
func LastMercuryRetrogradeStrict(jde float64) float64 {
@@ -553,258 +599,298 @@ func NextMercuryRetrogradeStrict(jde float64) float64 {
return NextMercuryRetrograde(jde)
}
func MercurySunElongation(jde float64) float64 {
lo1, bo1 := MercuryApparentLoBo(jde)
lo2 := HSunApparentLo(jde)
bo2 := HSunTrueBo(jde)
// MercurySunElongationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MercurySunElongationWithInstance(jde float64, inst *Instance) float64 {
lo1, bo1 := MercuryApparentLoBoWithInstance(jde, inst)
lo2 := HSunApparentLoWithInstance(jde, inst)
bo2 := HSunTrueBoWithInstance(jde, inst)
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)
func mercurySunElongationN(jde float64, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
lo1, bo1 := MercuryApparentLoBoNWithInstance(jde, n, inst)
lo2 := HSunApparentLoNWithInstance(jde, n, inst)
bo2 := HSunTrueBoNWithInstance(jde, n, inst)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
// mercuryGreatestElongationInWindow 求窗口内大距:目标是公开 MercurySunElongation 的极大(视距角),
// 而不是忽略光行差/视位置修正的真距角,否则返回的时刻不是调用方能量到的那个极值。
// 窗口两端是世界时,目标函数收力学时,因此逐次换算。
func mercuryGreatestElongationInWindow(start, end float64) float64 {
func mercuryGreatestElongationInWindow(start, end float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
return maximizeInWindow(start, end, 2.0, func(utJD float64) float64 {
return mercurySunElongationN(UTC2TT(utJD), mercuryEventSearchN)
return mercurySunElongationN(UTC2TT(utJD), mercuryEventSearchN, inst)
}, func(utJD float64) float64 {
return MercurySunElongation(UTC2TT(utJD))
return MercurySunElongationWithInstance(UTC2TT(utJD), inst)
})
}
func mercuryEastElongationWindowEndingAt(inferior float64) (float64, float64) {
lastSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(inferior))
func mercuryEastElongationWindowEndingAt(inferior float64, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
lastSuperior := LastMercurySuperiorConjunctionWithInstance(eventUTLastQueryTT(inferior), inst)
return lastSuperior + innerEventWindowPadding, inferior - innerEventWindowPadding
}
func mercuryWestElongationWindowEndingAt(superior float64) (float64, float64) {
lastInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(superior))
func mercuryWestElongationWindowEndingAt(superior float64, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
lastInferior := LastMercuryInferiorConjunctionWithInstance(eventUTLastQueryTT(superior), inst)
return lastInferior + innerEventWindowPadding, superior - innerEventWindowPadding
}
func mercuryEastElongationWindowContaining(jde float64) (float64, float64) {
nextInferior := NextMercuryInferiorConjunction(jde)
start, end := mercuryEastElongationWindowEndingAt(nextInferior)
func mercuryEastElongationWindowContaining(jde float64, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
nextInferior := NextMercuryInferiorConjunctionWithInstance(jde, inst)
start, end := mercuryEastElongationWindowEndingAt(nextInferior, inst)
if eventUTQueryBeforeOrEqual(start, jde) {
return start, end
}
currentInferior := LastMercuryInferiorConjunction(jde)
return mercuryEastElongationWindowEndingAt(currentInferior)
currentInferior := LastMercuryInferiorConjunctionWithInstance(jde, inst)
return mercuryEastElongationWindowEndingAt(currentInferior, inst)
}
func mercuryWestElongationWindowContaining(jde float64) (float64, float64) {
nextSuperior := NextMercurySuperiorConjunction(jde)
start, end := mercuryWestElongationWindowEndingAt(nextSuperior)
func mercuryWestElongationWindowContaining(jde float64, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
nextSuperior := NextMercurySuperiorConjunctionWithInstance(jde, inst)
start, end := mercuryWestElongationWindowEndingAt(nextSuperior, inst)
if eventUTQueryBeforeOrEqual(start, jde) {
return start, end
}
currentSuperior := LastMercurySuperiorConjunction(jde)
return mercuryWestElongationWindowEndingAt(currentSuperior)
currentSuperior := LastMercurySuperiorConjunctionWithInstance(jde, inst)
return mercuryWestElongationWindowEndingAt(currentSuperior, inst)
}
func nextMercuryGreatestElongationTyped(jde float64, east bool) float64 {
func nextMercuryGreatestElongationTyped(jde float64, east bool, instances ...*Instance) float64 {
inst := instanceValue(instances)
if !isFiniteFloat(jde) {
return math.NaN()
}
if east {
start, windowEnd := mercuryEastElongationWindowContaining(jde)
start, windowEnd := mercuryEastElongationWindowContaining(jde, inst)
for i := 0; i < eventDirectionalSearchIterations; i++ {
date := mercuryGreatestElongationInWindow(start, windowEnd)
date := mercuryGreatestElongationInWindow(start, windowEnd, inst)
if !isFiniteFloat(date) {
return math.NaN()
}
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextInferior := NextMercuryInferiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = mercuryEastElongationWindowEndingAt(nextInferior)
nextInferior := NextMercuryInferiorConjunctionWithInstance(eventUTNextQueryTT(windowEnd), inst)
start, windowEnd = mercuryEastElongationWindowEndingAt(nextInferior, inst)
}
return math.NaN()
}
start, windowEnd := mercuryWestElongationWindowContaining(jde)
start, windowEnd := mercuryWestElongationWindowContaining(jde, inst)
for i := 0; i < eventDirectionalSearchIterations; i++ {
date := mercuryGreatestElongationInWindow(start, windowEnd)
date := mercuryGreatestElongationInWindow(start, windowEnd, inst)
if !isFiniteFloat(date) {
return math.NaN()
}
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextSuperior := NextMercurySuperiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = mercuryWestElongationWindowEndingAt(nextSuperior)
nextSuperior := NextMercurySuperiorConjunctionWithInstance(eventUTNextQueryTT(windowEnd), inst)
start, windowEnd = mercuryWestElongationWindowEndingAt(nextSuperior, inst)
}
return math.NaN()
}
func lastMercuryGreatestElongationTyped(jde float64, east bool) float64 {
func lastMercuryGreatestElongationTyped(jde float64, east bool, instances ...*Instance) float64 {
inst := instanceValue(instances)
if !isFiniteFloat(jde) {
return math.NaN()
}
if east {
start, windowEnd := mercuryEastElongationWindowContaining(jde)
start, windowEnd := mercuryEastElongationWindowContaining(jde, inst)
for i := 0; i < eventDirectionalSearchIterations; i++ {
date := mercuryGreatestElongationInWindow(start, windowEnd)
date := mercuryGreatestElongationInWindow(start, windowEnd, inst)
if !isFiniteFloat(date) {
return math.NaN()
}
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = mercuryEastElongationWindowEndingAt(prevInferior)
prevInferior := LastMercuryInferiorConjunctionWithInstance(eventUTLastQueryTT(start), inst)
start, windowEnd = mercuryEastElongationWindowEndingAt(prevInferior, inst)
}
return math.NaN()
}
start, windowEnd := mercuryWestElongationWindowContaining(jde)
start, windowEnd := mercuryWestElongationWindowContaining(jde, inst)
for i := 0; i < eventDirectionalSearchIterations; i++ {
date := mercuryGreatestElongationInWindow(start, windowEnd)
date := mercuryGreatestElongationInWindow(start, windowEnd, inst)
if !isFiniteFloat(date) {
return math.NaN()
}
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = mercuryWestElongationWindowEndingAt(prevSuperior)
prevSuperior := LastMercurySuperiorConjunctionWithInstance(eventUTLastQueryTT(start), inst)
start, windowEnd = mercuryWestElongationWindowEndingAt(prevSuperior, inst)
}
return math.NaN()
}
// mercuryElongationWindowAt 返回包含 jde 的该侧大距窗口;查询落在合的 4 秒内边距里时两侧都不包含。
func mercuryElongationWindowAt(jde float64, east bool) (float64, float64, bool) {
func mercuryElongationWindowAt(jde float64, east bool, instances ...*Instance) (float64, float64, bool) {
inst := instanceValue(instances)
if east {
start, end := mercuryEastElongationWindowEndingAt(NextMercuryInferiorConjunction(jde))
start, end := mercuryEastElongationWindowEndingAt(NextMercuryInferiorConjunctionWithInstance(jde, inst), inst)
return start, end, eventUTQueryBeforeOrEqual(start, jde) && eventUTQueryAfterOrEqual(end, jde)
}
start, end := mercuryWestElongationWindowEndingAt(NextMercurySuperiorConjunction(jde))
start, end := mercuryWestElongationWindowEndingAt(NextMercurySuperiorConjunctionWithInstance(jde, inst), inst)
return start, end, eventUTQueryBeforeOrEqual(start, jde) && eventUTQueryAfterOrEqual(end, jde)
}
// 无东西侧参数的 Next/Last 先只看查询所在窗口那一侧:该侧大距未过就是答案,已过则另一侧紧接着的
// 下一个才是答案,因此只有在查询贴住合(两侧窗口都不含)时才退化为两侧都算。赤经差在合附近会提前
// 变号,不能用它定窗口。
func NextMercuryGreatestElongation(jde float64) float64 {
// NextMercuryGreatestElongationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryGreatestElongationWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
// 先只看查询所在一侧的窗口,未过即答案,已过则取另一侧的下一个;赤经差在合附近会提前变号,不能用来定窗口。
for _, east := range [2]bool{true, false} {
start, end, ok := mercuryElongationWindowAt(jde, east)
start, end, ok := mercuryElongationWindowAt(jde, east, inst)
if !ok {
continue
}
if date := mercuryGreatestElongationInWindow(start, end); isFiniteFloat(date) && eventUTQueryAfterOrEqual(date, jde) {
if date := mercuryGreatestElongationInWindow(start, end, inst); isFiniteFloat(date) && eventUTQueryAfterOrEqual(date, jde) {
return date
}
if date := nextMercuryGreatestElongationTyped(jde, !east); isFiniteFloat(date) && eventUTQueryAfterOrEqual(date, jde) {
if date := nextMercuryGreatestElongationTyped(jde, !east, inst); isFiniteFloat(date) && eventUTQueryAfterOrEqual(date, jde) {
return date
}
break
}
return earliestFiniteEventUT(nextMercuryGreatestElongationTyped(jde, true), nextMercuryGreatestElongationTyped(jde, false))
return earliestFiniteEventUT(nextMercuryGreatestElongationTyped(jde, true, inst), nextMercuryGreatestElongationTyped(jde, false, inst))
}
func LastMercuryGreatestElongation(jde float64) float64 {
// LastMercuryGreatestElongationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryGreatestElongationWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
// 先只看查询所在一侧的窗口,未过即答案,已过则取另一侧的下一个;赤经差在合附近会提前变号,不能用来定窗口。
for _, east := range [2]bool{true, false} {
start, end, ok := mercuryElongationWindowAt(jde, east)
start, end, ok := mercuryElongationWindowAt(jde, east, inst)
if !ok {
continue
}
if date := mercuryGreatestElongationInWindow(start, end); isFiniteFloat(date) && eventUTQueryBeforeOrEqual(date, jde) {
if date := mercuryGreatestElongationInWindow(start, end, inst); isFiniteFloat(date) && eventUTQueryBeforeOrEqual(date, jde) {
return date
}
if date := lastMercuryGreatestElongationTyped(jde, !east); isFiniteFloat(date) && eventUTQueryBeforeOrEqual(date, jde) {
if date := lastMercuryGreatestElongationTyped(jde, !east, inst); isFiniteFloat(date) && eventUTQueryBeforeOrEqual(date, jde) {
return date
}
break
}
return latestFiniteEventUT(lastMercuryGreatestElongationTyped(jde, true), lastMercuryGreatestElongationTyped(jde, false))
return latestFiniteEventUT(lastMercuryGreatestElongationTyped(jde, true, inst), lastMercuryGreatestElongationTyped(jde, false, inst))
}
func LastMercuryInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryInferiorConjunction, NextMercuryInferiorConjunction)
// LastMercuryInferiorConjunctionInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryInferiorConjunctionInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastSimpleEvent(jde, func(jde float64) float64 { return LastMercuryInferiorConjunctionWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryInferiorConjunctionWithInstance(jde, inst) })
}
func NextMercuryInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryInferiorConjunction, NextMercuryInferiorConjunction)
// NextMercuryInferiorConjunctionInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryInferiorConjunctionInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextSimpleEvent(jde, func(jde float64) float64 { return LastMercuryInferiorConjunctionWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryInferiorConjunctionWithInstance(jde, inst) })
}
func LastMercurySuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercurySuperiorConjunction, NextMercurySuperiorConjunction)
// LastMercurySuperiorConjunctionInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercurySuperiorConjunctionInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastSimpleEvent(jde, func(jde float64) float64 { return LastMercurySuperiorConjunctionWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercurySuperiorConjunctionWithInstance(jde, inst) })
}
func NextMercurySuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercurySuperiorConjunction, NextMercurySuperiorConjunction)
// NextMercurySuperiorConjunctionInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercurySuperiorConjunctionInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextSimpleEvent(jde, func(jde float64) float64 { return LastMercurySuperiorConjunctionWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercurySuperiorConjunctionWithInstance(jde, inst) })
}
func LastMercuryRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryRetrograde, NextMercuryRetrograde)
// LastMercuryRetrogradeInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryRetrogradeInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastSimpleEvent(jde, func(jde float64) float64 { return LastMercuryRetrogradeWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryRetrogradeWithInstance(jde, inst) })
}
func NextMercuryRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryRetrograde, NextMercuryRetrograde)
// NextMercuryRetrogradeInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryRetrogradeInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextSimpleEvent(jde, func(jde float64) float64 { return LastMercuryRetrogradeWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryRetrogradeWithInstance(jde, inst) })
}
func LastMercuryProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryProgradeToRetrograde, NextMercuryProgradeToRetrograde)
// LastMercuryProgradeToRetrogradeInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryProgradeToRetrogradeInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastSimpleEvent(jde, func(jde float64) float64 { return LastMercuryProgradeToRetrogradeWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryProgradeToRetrogradeWithInstance(jde, inst) })
}
func NextMercuryProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryProgradeToRetrograde, NextMercuryProgradeToRetrograde)
// NextMercuryProgradeToRetrogradeInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryProgradeToRetrogradeInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextSimpleEvent(jde, func(jde float64) float64 { return LastMercuryProgradeToRetrogradeWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryProgradeToRetrogradeWithInstance(jde, inst) })
}
func LastMercuryRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryRetrogradeToPrograde, NextMercuryRetrogradeToPrograde)
// LastMercuryRetrogradeToProgradeInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryRetrogradeToProgradeInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastSimpleEvent(jde, func(jde float64) float64 { return LastMercuryRetrogradeToProgradeWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryRetrogradeToProgradeWithInstance(jde, inst) })
}
func NextMercuryRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryRetrogradeToPrograde, NextMercuryRetrogradeToPrograde)
// NextMercuryRetrogradeToProgradeInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryRetrogradeToProgradeInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextSimpleEvent(jde, func(jde float64) float64 { return LastMercuryRetrogradeToProgradeWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryRetrogradeToProgradeWithInstance(jde, inst) })
}
func LastMercuryGreatestElongationInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongation, NextMercuryGreatestElongation)
// LastMercuryGreatestElongationInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryGreatestElongationInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastSimpleEvent(jde, func(jde float64) float64 { return LastMercuryGreatestElongationWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryGreatestElongationWithInstance(jde, inst) })
}
func NextMercuryGreatestElongationInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongation, NextMercuryGreatestElongation)
// NextMercuryGreatestElongationInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryGreatestElongationInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextSimpleEvent(jde, func(jde float64) float64 { return LastMercuryGreatestElongationWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryGreatestElongationWithInstance(jde, inst) })
}
func LastMercuryGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongationEast, NextMercuryGreatestElongationEast)
// LastMercuryGreatestElongationEastInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryGreatestElongationEastInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastSimpleEvent(jde, func(jde float64) float64 { return LastMercuryGreatestElongationEastWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryGreatestElongationEastWithInstance(jde, inst) })
}
func NextMercuryGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongationEast, NextMercuryGreatestElongationEast)
// NextMercuryGreatestElongationEastInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryGreatestElongationEastInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextSimpleEvent(jde, func(jde float64) float64 { return LastMercuryGreatestElongationEastWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryGreatestElongationEastWithInstance(jde, inst) })
}
func LastMercuryGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongationWest, NextMercuryGreatestElongationWest)
// LastMercuryGreatestElongationWestInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryGreatestElongationWestInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastSimpleEvent(jde, func(jde float64) float64 { return LastMercuryGreatestElongationWestWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryGreatestElongationWestWithInstance(jde, inst) })
}
func NextMercuryGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongationWest, NextMercuryGreatestElongationWest)
// NextMercuryGreatestElongationWestInclusiveWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryGreatestElongationWestInclusiveWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextSimpleEvent(jde, func(jde float64) float64 { return LastMercuryGreatestElongationWestWithInstance(jde, inst) }, func(jde float64) float64 { return NextMercuryGreatestElongationWestWithInstance(jde, inst) })
}
func NextMercuryGreatestElongationEast(jde float64) float64 {
return nextMercuryGreatestElongationTyped(jde, true)
// NextMercuryGreatestElongationEastWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryGreatestElongationEastWithInstance(jde float64, inst *Instance) float64 {
return nextMercuryGreatestElongationTyped(jde, true, inst)
}
func NextMercuryGreatestElongationWest(jde float64) float64 {
return nextMercuryGreatestElongationTyped(jde, false)
// NextMercuryGreatestElongationWestWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMercuryGreatestElongationWestWithInstance(jde float64, inst *Instance) float64 {
return nextMercuryGreatestElongationTyped(jde, false, inst)
}
func LastMercuryGreatestElongationEast(jde float64) float64 {
return lastMercuryGreatestElongationTyped(jde, true)
// LastMercuryGreatestElongationEastWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryGreatestElongationEastWithInstance(jde float64, inst *Instance) float64 {
return lastMercuryGreatestElongationTyped(jde, true, inst)
}
func LastMercuryGreatestElongationWest(jde float64) float64 {
return lastMercuryGreatestElongationTyped(jde, false)
// LastMercuryGreatestElongationWestWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMercuryGreatestElongationWestWithInstance(jde float64, inst *Instance) float64 {
return lastMercuryGreatestElongationTyped(jde, false, inst)
}
+22 -16
View File
@@ -1,16 +1,18 @@
package basic
import . "b612.me/astro/tools"
import (
. "b612.me/astro/tools"
)
// MoonBrightLimbPositionAngle 月亮明亮边缘位置角 / position angle of the Moon's bright limb.
func MoonBrightLimbPositionAngle(jde float64) float64 {
return MoonBrightLimbPositionAngleN(jde, -1)
}
// MoonBrightLimbPositionAngleN 月亮明亮边缘位置角(截断版) / truncated position angle of the Moon's bright limb.
func MoonBrightLimbPositionAngleN(jde float64, n int) float64 {
sunRA, sunDec := HSunApparentRaDecN(jde, n)
moonRA, moonDec := HMoonTrueRaDecN(jde, n)
// MoonBrightLimbPositionAngleNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonBrightLimbPositionAngleNWithInstance(jde float64, n int, inst *Instance) float64 {
sunRA, sunDec := HSunApparentRaDecNWithInstance(jde, n, inst)
moonRA, moonDec := HMoonTrueRaDecNWithInstance(jde, n, inst)
return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec)
}
@@ -19,23 +21,27 @@ func MoonTopocentricBrightLimbPositionAngle(jde, observerLon, observerLat, heigh
return MoonTopocentricBrightLimbPositionAngleN(jde, observerLon, observerLat, height, -1)
}
// MoonTopocentricBrightLimbPositionAngleN 月亮站心明亮边缘位置角(截断版) / truncated topocentric position angle of the Moon's bright limb.
func MoonTopocentricBrightLimbPositionAngleN(jde, observerLon, observerLat, height float64, n int) float64 {
sunRA, sunDec := sunTopocentricApparentRaDecN(jde, observerLon, observerLat, height, n)
moonRA, moonDec := moonTopocentricApparentRaDecN(jde, observerLon, observerLat, height, n)
// MoonTopocentricBrightLimbPositionAngleNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonTopocentricBrightLimbPositionAngleNWithInstance(jde, observerLon, observerLat, height float64, n int, inst *Instance) float64 {
sunRA, sunDec := sunTopocentricApparentRaDecN(jde, observerLon, observerLat, height, n, inst)
moonRA, moonDec := moonTopocentricApparentRaDecN(jde, observerLon, observerLat, height, n, inst)
return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec)
}
func moonTopocentricApparentRaDecN(jde, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA := HMoonTrueRaN(jde, n)
geocentricDec := HMoonTrueDecN(jde, n)
distanceAU := HMoonAwayN(jde, n) / moonPhysicalAstronomicalUnitKM
func moonTopocentricApparentRaDecN(jde, observerLon, observerLat, height float64, n int, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
geocentricRA := HMoonTrueRaNWithInstance(jde, n, inst)
geocentricDec := HMoonTrueDecNWithInstance(jde, n, inst)
distanceAU := HMoonAwayNWithInstance(jde, n, inst) / moonPhysicalAstronomicalUnitKM
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TT2UTC(jde), distanceAU, height)
}
func sunTopocentricApparentRaDecN(jde, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA, geocentricDec := HSunApparentRaDecN(jde, n)
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TT2UTC(jde), EarthAwayN(jde, n), height)
func sunTopocentricApparentRaDecN(jde, observerLon, observerLat, height float64, n int, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
geocentricRA, geocentricDec := HSunApparentRaDecNWithInstance(jde, n, inst)
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TT2UTC(jde), EarthAwayNWithInstance(jde, n, inst), height)
}
func brightLimbPositionAngleFromRaDec(sunRA, sunDec, bodyRA, bodyDec float64) float64 {
+107
View File
@@ -0,0 +1,107 @@
package basic
import (
"math"
"b612.me/astro/internal/ephem"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
func moonCalcNewNWithInstance(coordIndex int, jd float64, n int, inst *Instance) float64 {
if inst == nil {
return planet.WhereMoonN(coordIndex, jd, n)
}
if inst.Model() != ephem.Custom {
return planet.WhereMoonModelN(inst.Model(), coordIndex, jd, n)
}
if lo, bo, distAU, ok := instanceStateEcliptic(ephem.Moon, jd, inst); ok {
switch coordIndex {
case 0:
return lo * math.Pi / 180
case 1:
return bo * math.Pi / 180
case 2:
return distAU * astronomicalUnitKM
}
}
return math.NaN()
}
// MoonCalcNewWithInstance 指定星历实例的月球几何量 / lunar geometry with an explicit ephemeris instance.
func MoonCalcNewWithInstance(coordIndex int, jd float64, inst *Instance) float64 {
return moonCalcNewNWithInstance(coordIndex, jd, -1, inst)
}
// MoonCalcNewNWithInstance 指定星历实例的月球几何量(n 变体) / n-parameterised lunar geometry with an explicit ephemeris instance.
func MoonCalcNewNWithInstance(coordIndex int, jd float64, n int, inst *Instance) float64 {
return moonCalcNewNWithInstance(coordIndex, jd, n, inst)
}
// HMoonTrueLoWithInstance 指定星历实例的月球真黄经 / true lunar longitude with an explicit ephemeris instance.
func HMoonTrueLoWithInstance(jde float64, inst *Instance) float64 {
return HMoonTrueLoNWithInstance(jde, -1, inst)
}
// HMoonTrueLoNWithInstance 指定星历实例的月球真黄经(n 变体) / n-parameterised true lunar longitude with an explicit ephemeris instance.
func HMoonTrueLoNWithInstance(jde float64, n int, inst *Instance) float64 {
return Limit360(moonCalcNewNWithInstance(0, jde, n, inst) * 180 / math.Pi)
}
// HMoonTrueBoWithInstance 指定星历实例的月球真黄纬 / true lunar latitude with an explicit ephemeris instance.
func HMoonTrueBoWithInstance(jde float64, inst *Instance) float64 {
return HMoonTrueBoNWithInstance(jde, -1, inst)
}
// HMoonTrueBoNWithInstance 指定星历实例的月球真黄纬(n 变体) / n-parameterised true lunar latitude with an explicit ephemeris instance.
func HMoonTrueBoNWithInstance(jde float64, n int, inst *Instance) float64 {
return moonCalcNewNWithInstance(1, jde, n, inst) * 180 / math.Pi
}
// HMoonAwayWithInstance 指定星历实例的月地距离 / lunar distance with an explicit ephemeris instance.
func HMoonAwayWithInstance(jde float64, inst *Instance) float64 {
return HMoonAwayNWithInstance(jde, -1, inst)
}
// HMoonAwayNWithInstance 指定星历实例的月地距离(n 变体) / n-parameterised lunar distance with an explicit ephemeris instance.
func HMoonAwayNWithInstance(jde float64, n int, inst *Instance) float64 {
return moonCalcNewNWithInstance(2, jde, n, inst)
}
// HMoonApparentLoWithInstance 指定星历实例的月球视黄经 / apparent lunar longitude with an explicit ephemeris instance.
func HMoonApparentLoWithInstance(jde float64, inst *Instance) float64 {
return HMoonApparentLoNWithInstance(jde, -1, inst)
}
// HMoonApparentLoNWithInstance 指定星历实例的月球视黄经(n 变体) / n-parameterised apparent lunar longitude with an explicit ephemeris instance.
func HMoonApparentLoNWithInstance(jde float64, n int, inst *Instance) float64 {
if lo, _, _, _, ok, err := vectorApparentWithInstance(ephem.Moon, jde, n, inst); err != nil {
return math.NaN()
} else if ok {
return lo
}
return HMoonTrueLoNWithInstance(jde, n, inst) + Nutation2000Bi(jde)
}
// HMoonGeocentricApparentRaDecWithInstance 指定星历实例的月球地心视赤经纬 / apparent geocentric lunar right ascension and declination with an explicit ephemeris instance.
func HMoonGeocentricApparentRaDecWithInstance(jde float64, inst *Instance) (float64, float64) {
return HMoonGeocentricApparentRaDecNWithInstance(jde, -1, inst)
}
// HMoonGeocentricApparentRaDecNWithInstance 指定星历实例的月球地心视赤经纬(n 变体) / n-parameterised apparent geocentric lunar right ascension and declination with an explicit ephemeris instance.
func HMoonGeocentricApparentRaDecNWithInstance(jde float64, n int, inst *Instance) (float64, float64) {
return moonApparentRaDecN(jde, n, inst)
}
// HMoonTrueRaDecWithInstance 指定星历实例的月球真赤经纬 / true lunar right ascension and declination with an explicit ephemeris instance.
func HMoonTrueRaDecWithInstance(jde float64, inst *Instance) (float64, float64) {
return moonApparentRaDecN(jde, -1, inst)
}
// HMoonApparentRaDecWithInstance 指定星历实例的月球站心视赤经纬 / topocentric apparent lunar right ascension and declination with an explicit ephemeris instance.
func HMoonApparentRaDecWithInstance(jd, lon, lat, tz float64, inst *Instance) (float64, float64) {
jde := UTC2TT(jd) - tz/24
ra, dec := HMoonTrueRaDecWithInstance(jde, inst)
away := HMoonAwayWithInstance(jde, inst) / astronomicalUnitKM
return TopocentricRaDec(ra, dec, lat, lon, jd-tz/24, away, 0)
}
+5 -9
View File
@@ -1,15 +1,11 @@
package basic
// MoonHorizon 返回 UTC 儒略日下海平面几何月球中心地平圈(月球恰好在地平线上的观测者轨迹,
// 即月下点周围的地平圈)的 [经度, 纬度] 顶点,单位为度,不重复首点。视差与椭球口径同
// HMoonHeight,不含折射;与 HMoonHeight(经, 纬, ..., 0) 配合时该圈上的点高度角为 0。
// samples<=0 取 360,其余夹到 [12, 1440]。
// MoonHorizon returns sea-level geometric Moon-centre horizon vertices in degrees for a UTC Julian
// day: the locus of observers that see the Moon exactly on the horizon. Parallax and the observer
// ellipsoid match HMoonHeight; refraction is excluded.
func MoonHorizon(jdUTC float64, samples int) [][2]float64 {
import ()
// MoonHorizonWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonHorizonWithInstance(jdUTC float64, samples int, inst *Instance) [][2]float64 {
if !finite(jdUTC) {
return nil
}
return MoonStateAt(jdUTC).MoonHorizon(samples)
return MoonStateAtWithInstance(jdUTC, inst).MoonHorizon(samples)
}
+48 -38
View File
@@ -81,47 +81,49 @@ var moonMaxDeclinationSouthCoefficients = moonMaxDeclinationCoefficients{
},
}
// MoonMaximumNorthDeclinations 指定年月内的所有月球最大北赤纬事件 / all maximum northern lunar declination events in the given Gregorian month.
func MoonMaximumNorthDeclinations(year int, month time.Month) []DeclinationEvent {
return moonMaximumDeclinationsInMonth(year, month, moonMaxDeclinationNorthCoefficients)
// MoonMaximumNorthDeclinationsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonMaximumNorthDeclinationsWithInstance(year int, month time.Month, inst *Instance) []DeclinationEvent {
return moonMaximumDeclinationsInMonth(year, month, moonMaxDeclinationNorthCoefficients, inst)
}
// MoonMaximumSouthDeclinations 指定年月内的所有月球最大南赤纬事件 / all maximum southern lunar declination events in the given Gregorian month.
func MoonMaximumSouthDeclinations(year int, month time.Month) []DeclinationEvent {
return moonMaximumDeclinationsInMonth(year, month, moonMaxDeclinationSouthCoefficients)
// MoonMaximumSouthDeclinationsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonMaximumSouthDeclinationsWithInstance(year int, month time.Month, inst *Instance) []DeclinationEvent {
return moonMaximumDeclinationsInMonth(year, month, moonMaxDeclinationSouthCoefficients, inst)
}
// LastMoonMaximumNorthDeclination 指定时刻之前最近一次月球最大北赤纬 / last maximum northern lunar declination at or before jd.
func LastMoonMaximumNorthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationNorthCoefficients, -1, true)
// LastMoonMaximumNorthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMoonMaximumNorthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationNorthCoefficients, -1, true, inst)
}
// NextMoonMaximumNorthDeclination 指定时刻之后最近一次月球最大北赤纬 / next maximum northern lunar declination after jd.
func NextMoonMaximumNorthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationNorthCoefficients, 1, false)
// NextMoonMaximumNorthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMoonMaximumNorthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationNorthCoefficients, 1, false, inst)
}
// ClosestMoonMaximumNorthDeclination 离指定时刻最近一次月球最大北赤纬 / closest maximum northern lunar declination to jd.
func ClosestMoonMaximumNorthDeclination(jd float64) DeclinationEvent {
return moonClosestMaximumDeclination(jd, moonMaxDeclinationNorthCoefficients)
// ClosestMoonMaximumNorthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func ClosestMoonMaximumNorthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonClosestMaximumDeclination(jd, moonMaxDeclinationNorthCoefficients, inst)
}
// LastMoonMaximumSouthDeclination 指定时刻之前最近一次月球最大南赤纬 / last maximum southern lunar declination at or before jd.
func LastMoonMaximumSouthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationSouthCoefficients, -1, true)
// LastMoonMaximumSouthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMoonMaximumSouthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationSouthCoefficients, -1, true, inst)
}
// NextMoonMaximumSouthDeclination 指定时刻之后最近一次月球最大南赤纬 / next maximum southern lunar declination after jd.
func NextMoonMaximumSouthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationSouthCoefficients, 1, false)
// NextMoonMaximumSouthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMoonMaximumSouthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationSouthCoefficients, 1, false, inst)
}
// ClosestMoonMaximumSouthDeclination 离指定时刻最近一次月球最大南赤纬 / closest maximum southern lunar declination to jd.
func ClosestMoonMaximumSouthDeclination(jd float64) DeclinationEvent {
return moonClosestMaximumDeclination(jd, moonMaxDeclinationSouthCoefficients)
// ClosestMoonMaximumSouthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func ClosestMoonMaximumSouthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonClosestMaximumDeclination(jd, moonMaxDeclinationSouthCoefficients, inst)
}
func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDeclinationCoefficients) []DeclinationEvent {
func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDeclinationCoefficients, instances ...*Instance) []DeclinationEvent {
inst := instanceValue(instances)
startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
endUTC := startUTC.AddDate(0, 1, 0)
startTT := UTC2TT(Date2JD(startUTC))
@@ -141,7 +143,7 @@ func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDe
events := make([]DeclinationEvent, 0, 2)
for k := kStart; k <= kEnd; k++ {
event := moonMaximumDeclinationEvent(k, coeffs, cfg)
event := moonMaximumDeclinationEvent(k, coeffs, cfg, inst)
eventTimeUTC := JD2DateByZone(event.JD, time.UTC, false)
if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) {
continue
@@ -155,10 +157,12 @@ func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDe
return events
}
func moonMaximumDeclinationEvent(k int, coeffs moonMaxDeclinationCoefficients, cfg apsisSearchConfig) DeclinationEvent {
func moonMaximumDeclinationEvent(k int, coeffs moonMaxDeclinationCoefficients, cfg apsisSearchConfig, instances ...*Instance) DeclinationEvent {
inst := instanceValue(instances)
seedTT := moonMaximumDeclinationSeedTT(k, coeffs)
eventTT, declination := refineDistanceExtremum(seedTT, cfg, func(sampleTT float64) float64 {
return HMoonTrueDecN(sampleTT, -1)
return HMoonTrueDecNWithInstance(sampleTT, -1, inst)
})
return DeclinationEvent{
JD: TT2UTC(eventTT),
@@ -166,7 +170,9 @@ func moonMaximumDeclinationEvent(k int, coeffs moonMaxDeclinationCoefficients, c
}
}
func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficients, direction int, includeCurrent bool) DeclinationEvent {
func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficients, direction int, includeCurrent bool, instances ...*Instance) DeclinationEvent {
inst := instanceValue(instances)
if !finite(jd) {
// 非有限查询算不出周期序号(centerK 会把 NaN/±Inf 压成 MinInt64,种子外推到 1e40 后步长小于一个 ULP)。
return DeclinationEvent{}
@@ -177,12 +183,12 @@ func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficie
if direction < 0 {
step = -1
}
event := moonMaximumDeclinationEvent(centerK, coeffs, cfg)
event := moonMaximumDeclinationEvent(centerK, coeffs, cfg, inst)
// 事件时刻随周期序号单调递增:该方向上最近的候选就是 centerK 沿该方向第一个满足方向不变量
// 的周期;命中后只需再回退检查更早的周期是否同样满足,可达范围仍旧是 ±3 个周期。
if !moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
event = moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg)
event = moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg, inst)
if moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
return event
}
@@ -190,7 +196,7 @@ func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficie
return DeclinationEvent{}
}
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
previous := moonMaximumDeclinationEvent(centerK-step*offset, coeffs, cfg)
previous := moonMaximumDeclinationEvent(centerK-step*offset, coeffs, cfg, inst)
if !moonMaximumDeclinationMatchesDirection(previous.JD-jd, direction, includeCurrent) {
break
}
@@ -199,20 +205,22 @@ func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficie
return event
}
func moonClosestMaximumDeclination(jd float64, coeffs moonMaxDeclinationCoefficients) DeclinationEvent {
func moonClosestMaximumDeclination(jd float64, coeffs moonMaxDeclinationCoefficients, instances ...*Instance) DeclinationEvent {
inst := instanceValue(instances)
if !finite(jd) {
return DeclinationEvent{}
}
cfg := moonMaximumDeclinationSearchConfig(coeffs)
centerK := moonMaximumDeclinationOffset(jd, coeffs)
center := moonMaximumDeclinationEvent(centerK, coeffs, cfg)
center := moonMaximumDeclinationEvent(centerK, coeffs, cfg, inst)
var last, next DeclinationEvent
if center.JD <= jd {
last = center
next = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, 1, false)
next = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, 1, false, inst)
} else {
next = center
last = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, -1, true)
last = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, -1, true, inst)
}
lastDistance := math.Abs(jd - last.JD)
nextDistance := math.Abs(next.JD - jd)
@@ -224,13 +232,15 @@ func moonClosestMaximumDeclination(jd float64, coeffs moonMaxDeclinationCoeffici
// moonMaximumDeclinationAround 从 centerK 沿 step 方向找第一个满足方向不变量的周期;±3 个周期内没有就返回零事件。
func moonMaximumDeclinationAround(jd float64, coeffs moonMaxDeclinationCoefficients, cfg apsisSearchConfig,
centerK, step int, includeCurrent bool) DeclinationEvent {
centerK, step int, includeCurrent bool, instances ...*Instance) DeclinationEvent {
inst := instanceValue(instances)
direction := 1
if step < 0 {
direction = -1
}
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
event := moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg)
event := moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg, inst)
if moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
return event
}
+3
View File
@@ -47,6 +47,9 @@ func moonMaximumDeclinationReferenceClosest(jd float64, coeffs moonMaxDeclinatio
}
func TestMoonMaximumDeclinationPruningMatchesFullSearch(t *testing.T) {
if testing.Short() {
t.Skip("slow regression")
}
for _, coeffs := range []moonMaxDeclinationCoefficients{moonMaxDeclinationNorthCoefficients, moonMaxDeclinationSouthCoefficients} {
for jd := JDCalc(2024, 1, 1); jd <= JDCalc(2025, 6, 1); jd += 2.5 {
for _, direction := range []struct {
-1
View File
@@ -24,7 +24,6 @@ type moonMaxDeclinationMonthState struct {
}
func TestMoonMaximumDeclinationsMatchHorizonsBaseline(t *testing.T) {
// Baseline is generated from JPL Horizons by scripts/generate_moon_max_declination_baseline.sh.
data, err := os.ReadFile("testdata/moon_max_declination_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
+76 -64
View File
@@ -55,15 +55,17 @@ func MoonHeight(jd, lon, lat, tz float64) float64 {
return ArcSin(tmp2)
}
func HMoonAzimuth(jd, lon, lat, tz float64) float64 {
return HMoonAzimuthN(jd, lon, lat, tz, -1)
// HMoonAzimuthWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func HMoonAzimuthWithInstance(jd, lon, lat, tz float64, inst *Instance) float64 {
return HMoonAzimuthNWithInstance(jd, lon, lat, tz, -1, inst)
}
func HMoonAzimuthN(jd, lon, lat, tz float64, n int) float64 {
// HMoonAzimuthNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func HMoonAzimuthNWithInstance(jd, lon, lat, tz float64, n int, inst *Instance) float64 {
jde := UTC2TT(jd - tz/24)
ra := HMoonTrueRaN(jde, n)
dec := HMoonTrueDecN(jde, n)
away := HMoonAwayN(jde, n) / 149597870.7
ra := HMoonTrueRaNWithInstance(jde, n, inst)
dec := HMoonTrueDecNWithInstance(jde, n, inst)
away := HMoonAwayNWithInstance(jde, n, inst) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
jdUT := jd - tz/24
@@ -86,15 +88,9 @@ func HMoonAzimuthN(jd, lon, lat, tz float64, n int) float64 {
}
}
// HMoonHeight 当地民用时儒略日下的月心几何高度角(度,不含折射)/ geometric Moon-centre altitude in degrees for a local civil Julian day.
//
// jd 是该时区的当地民用时(墙上时刻)儒略日,tz 是时区偏移小时数,库内按 jd−tz/24 换成 UTC。
// 只有 tz 给 0 时 jd 才是 UTC 儒略日;不要拿 UTC 数值再配非零 tz,那会多减一次时区。
// jd is that zone's local civil (wall-clock) Julian day and tz is the zone offset in hours,
// converted internally as jd-tz/24. Only tz 0 makes jd a UTC Julian day: pairing a UTC value with a
// non-zero tz subtracts the offset twice.
func HMoonHeight(jd, lon, lat, tz float64) float64 {
return HMoonHeightN(jd, lon, lat, tz, -1)
// HMoonHeightWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func HMoonHeightWithInstance(jd, lon, lat, tz float64, inst *Instance) float64 {
return HMoonHeightNWithInstance(jd, lon, lat, tz, -1, inst)
}
type moonObservationState struct {
@@ -102,10 +98,12 @@ type moonObservationState struct {
distanceKM float64
}
func hMoonObservationStateN(jd, lon, lat, tz, height float64, n int) moonObservationState {
func hMoonObservationStateN(jd, lon, lat, tz, height float64, n int, instances ...*Instance) moonObservationState {
inst := instanceValue(instances)
calculationJDE := UTC2TT(jd - tz/24)
ra, dec := HMoonTrueRaDecN(calculationJDE, n)
distanceKM := HMoonAwayN(calculationJDE, n)
ra, dec := HMoonTrueRaDecNWithInstance(calculationJDE, n, inst)
distanceKM := HMoonAwayNWithInstance(calculationJDE, n, inst)
distanceAU := distanceKM / angularDiameterAstronomicalUnitKM
topocentricRA, topocentricDec := TopocentricRaDec(ra, dec, lat, lon, jd-tz/24, distanceAU, height)
siderealTime := Limit360(ApparentSiderealTime(UTC2UT1(jd-tz/24))*15 + lon)
@@ -117,8 +115,9 @@ func hMoonObservationStateN(jd, lon, lat, tz, height float64, n int) moonObserva
}
}
func HMoonHeightN(jd, lon, lat, tz float64, n int) float64 {
return hMoonObservationStateN(jd, lon, lat, tz, 0, n).altitude
// HMoonHeightNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func HMoonHeightNWithInstance(jd, lon, lat, tz float64, n int, inst *Instance) float64 {
return hMoonObservationStateN(jd, lon, lat, tz, 0, n, inst).altitude
}
// MoonState 同一瞬间可对任意观测点复用的月球位置与恒星时 / one instant's lunar position and sidereal time, reusable across observers.
@@ -129,14 +128,14 @@ type MoonState struct {
siderealTime float64
}
// MoonStateAt 由 UTC 儒略日构造该瞬间的可复用月球状态 / builds the reusable state for one UTC Julian day.
func MoonStateAt(utcJD float64) MoonState {
// MoonStateAtWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonStateAtWithInstance(utcJD float64, inst *Instance) MoonState {
jde := UTC2TT(utcJD)
rightAscension, declination := HMoonTrueRaDec(jde)
rightAscension, declination := HMoonTrueRaDecWithInstance(jde, inst)
return MoonState{
rightAscension: rightAscension,
declination: declination,
distanceAU: HMoonAway(jde) / angularDiameterAstronomicalUnitKM,
distanceAU: HMoonAwayWithInstance(jde, inst) / angularDiameterAstronomicalUnitKM,
siderealTime: ApparentSiderealTime(UTC2UT1(utcJD)) * 15,
}
}
@@ -204,12 +203,17 @@ func (state MoonState) MoonHorizon(samples int) [][2]float64 {
return points
}
func moonRiseSetResidual(jd, longitude, latitude, timeZone, zenithShift, height float64, n int) float64 {
state := hMoonObservationStateN(jd, longitude, latitude, timeZone, height, n)
func moonRiseSetResidual(jd, longitude, latitude, timeZone, zenithShift, height float64, n int, instances ...*Instance) float64 {
return moonRiseSetResidualWeather(jd, longitude, latitude, timeZone, zenithShift, height,
refractionStandardPressureHPa, refractionStandardTemperatureC, n, instanceValue(instances))
}
func moonRiseSetResidualWeather(jd, longitude, latitude, timeZone, zenithShift, height, pressureHPa, temperatureC float64, n int, inst *Instance) float64 {
state := hMoonObservationStateN(jd, longitude, latitude, timeZone, height, n, inst)
// 相对观测者下沉地平线的视上缘高度角 / Apparent upper-limb altitude relative to the observer's depressed horizon.
residual := state.altitude + HeightDegreeByLat(height, latitude)
if zenithShift != 0 {
residual += RefractionFromTrueAltitude(state.altitude, refractionStandardPressureHPa, refractionStandardTemperatureC)
residual += RefractionFromTrueAltitude(state.altitude, pressureHPa, temperatureC)
residual += angularSemidiameterArcsec(moonEquatorialRadiusKM, state.distanceKM) / 3600
}
return residual
@@ -218,13 +222,15 @@ func moonRiseSetResidual(jd, longitude, latitude, timeZone, zenithShift, height
// moonRiseSetOnCivilDay 在民用日内求升/落时刻;找不到过零时的错误口径与 rise_set.go 的 ErrNeverRise/ErrNeverSet 一致,
// fallbackErr 是调用方用中天/下中天残差预判的同一几何结论,命中时优先于扫描结果。
func moonRiseSetOnCivilDay(candidate, slope, civilDayStart, longitude, latitude, originalTimeZone,
localTimeZone, zenithShift, height float64, isRise bool, fallbackErr error) (float64, error) {
localTimeZone, zenithShift, height float64, isRise bool, fallbackErr error, instances ...*Instance) (float64, error) {
inst := instanceValue(instances)
if eventRiseSetCandidateValid(candidate, civilDayStart, slope, isRise) {
return candidate, nil
}
return eventDirectionalRiseSetSearch(civilDayStart, isRise, fallbackErr, func(outputJD float64) float64 {
localJD := outputJD + localTimeZone/24 - originalTimeZone/24
return moonRiseSetResidual(localJD, longitude, latitude, localTimeZone, zenithShift, height, -1)
return moonRiseSetResidual(localJD, longitude, latitude, localTimeZone, zenithShift, height, -1, inst)
})
}
@@ -248,12 +254,13 @@ func GetMoonTZTime(jd, lon, lat, tz float64) float64 { //实际中天时间{
return estimateJD
}
func MoonCulminationTime(localJD, lon, lat, timezone float64) float64 {
// MoonCulminationTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonCulminationTimeWithInstance(localJD, lon, lat, timezone float64, inst *Instance) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时;ra/dec 为瞬时天球坐标,非 J2000 等固定历元。
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-MoonTimeAngle(localJD, lon, lat, timezone))/15.0/24.0/0.9
estimateJD := localJD + Limit360(360-MoonTimeAngleWithInstance(localJD, lon, lat, timezone, inst))/15.0/24.0/0.9
limitHA := func(localJD, lon, timezone float64) float64 {
ha := MoonTimeAngle(localJD, lon, lat, timezone)
ha := MoonTimeAngleWithInstance(localJD, lon, lat, timezone, inst)
if ha < 180 {
ha += 360
}
@@ -271,16 +278,18 @@ func MoonCulminationTime(localJD, lon, lat, timezone float64) float64 {
return estimateJD
}
func MoonTimeAngle(jd, lon, lat, tz float64) float64 {
// MoonTimeAngleWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonTimeAngleWithInstance(jd, lon, lat, tz float64, inst *Instance) float64 {
startime := Limit360(ApparentSiderealTime(UTC2UT1(jd-tz/24))*15 + lon)
timeangle := startime - HMoonApparentRa(jd, lon, lat, tz)
timeangle := startime - HMoonApparentRaWithInstance(jd, lon, lat, tz, inst)
if timeangle < 0 {
timeangle += 360
}
return timeangle
}
func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) {
// GetMoonRiseTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func GetMoonRiseTimeWithInstance(julianDay, longitude, latitude, timeZone, zenithShift, height float64, inst *Instance) (float64, error) {
if !isFiniteFloat(julianDay) || !isFiniteFloat(longitude) || !isFiniteFloat(latitude) || !isFiniteFloat(timeZone) || !isFiniteFloat(zenithShift) || !isFiniteFloat(height) {
return 0, ErrInvalidObservationInput
}
@@ -291,11 +300,11 @@ func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, heig
// 时间分界线以传入的时区为准,不用当地时区,否则 0 时的判断会出错。
julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1)
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1, inst)
moonAngle := StandardAltitudeMoon(zenithShift, height, latitude)
moonAngleTime := MoonTimeAngle(julianDay, longitude, latitude, originalTimeZone)
moonAngleTime := MoonTimeAngleWithInstance(julianDay, longitude, latitude, originalTimeZone, inst)
if moonResidual > 0 { // 月亮在地平线上或在落下与下中天之间
if moonAngleTime > 180 {
@@ -314,32 +323,32 @@ func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, heig
estimatedTime += (timeToMeridian/24 + (timeToMeridian/24*12.0)/15.0/24.0)
}
currentAngle := MoonTimeAngle(estimatedTime, longitude, latitude, timeZone)
currentAngle := MoonTimeAngleWithInstance(estimatedTime, longitude, latitude, timeZone, inst)
if math.Abs(currentAngle-180) > 0.5 {
estimatedTime += (180 - currentAngle) * 4.0 / 60.0 / 24.0
}
currentResidual := moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1)
currentResidual := moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1, inst)
if !(currentResidual < -10 && math.Abs(latitude) < 60) {
if currentResidual > 0 {
// 下中天仍在地平线上:当日无落下(也无可升起),口径见 moonRiseSetOnCivilDay。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, true, ErrNeverSet)
originalTimeZone, timeZone, zenithShift, height, true, ErrNeverSet, inst)
}
checkTime := estimatedTime + 12.0/24.0 + 6.0/15.0/24.0
checkAngle := MoonTimeAngle(checkTime, longitude, latitude, timeZone)
checkAngle := MoonTimeAngleWithInstance(checkTime, longitude, latitude, timeZone, inst)
if checkAngle < 90 {
checkAngle += 360
}
checkTime += (360 - checkAngle) * 4.0 / 60.0 / 24.0
if moonRiseSetResidual(checkTime, longitude, latitude, timeZone, zenithShift, height, -1) < 0 {
if moonRiseSetResidual(checkTime, longitude, latitude, timeZone, zenithShift, height, -1, inst) < 0 {
// 上中天仍在地平线下:当日无升起。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, true, ErrNeverRise)
originalTimeZone, timeZone, zenithShift, height, true, ErrNeverRise, inst)
}
}
moonDeclination := MoonApparentDec(estimatedTime, longitude, latitude, timeZone)
moonDeclination := MoonApparentDecWithInstance(estimatedTime, longitude, latitude, timeZone, inst)
tmp := (Sin(moonAngle) - Sin(moonDeclination)*Sin(latitude)) / (Cos(moonDeclination) * Cos(latitude))
if math.Abs(tmp) <= 1 && latitude < 85 {
@@ -347,7 +356,7 @@ func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, heig
estimatedTime += hourAngle/24.00 + hourAngle/33.00/15.00
} else {
i := 0
for moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1) < 0 {
for moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1, inst) < 0 {
i++
estimatedTime += 15.0 / 60.0 / 24.0
if i > 48 {
@@ -357,13 +366,14 @@ func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, heig
}
// 使用牛顿迭代法求精确解
estimatedTime, slope := moonRiseSetResidualIteration(estimatedTime, longitude, latitude, timeZone, zenithShift, height, 0.00002)
estimatedTime, slope := moonRiseSetResidualIteration(estimatedTime, longitude, latitude, timeZone, zenithShift, height, 0.00002, inst)
estimatedTime = estimatedTime - timeZone/24 + originalTimeZone/24
return moonRiseSetOnCivilDay(estimatedTime, slope, civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, true, nil)
originalTimeZone, timeZone, zenithShift, height, true, nil, inst)
}
func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) {
// GetMoonSetTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func GetMoonSetTimeWithInstance(julianDay, longitude, latitude, timeZone, zenithShift, height float64, inst *Instance) (float64, error) {
if !isFiniteFloat(julianDay) || !isFiniteFloat(longitude) || !isFiniteFloat(latitude) || !isFiniteFloat(timeZone) || !isFiniteFloat(zenithShift) || !isFiniteFloat(height) {
return 0, ErrInvalidObservationInput
}
@@ -374,11 +384,11 @@ func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, heigh
// 时间分界线以传入的时区为准,不用当地时区,否则 0 时的判断会出错。
julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1)
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1, inst)
moonAngle := StandardAltitudeMoon(zenithShift, height, latitude)
moonAngleTime := MoonTimeAngle(julianDay, longitude, latitude, originalTimeZone)
moonAngleTime := MoonTimeAngleWithInstance(julianDay, longitude, latitude, originalTimeZone, inst)
if moonResidual < 0 {
timeToMeridian = (360 - moonAngleTime) / 15
@@ -394,7 +404,7 @@ func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, heigh
estimatedTime += (timeToMeridian/24.0 + (timeToMeridian/24.0*12.0)/15.0/24.0)
}
currentAngle := MoonTimeAngle(estimatedTime, longitude, latitude, timeZone)
currentAngle := MoonTimeAngleWithInstance(estimatedTime, longitude, latitude, timeZone, inst)
if currentAngle < 180 {
currentAngle += 360
}
@@ -403,24 +413,24 @@ func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, heigh
}
// estimatedTime = 月球中天时间
currentResidual := moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1)
currentResidual := moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1, inst)
if !(currentResidual > 10 && math.Abs(latitude) < 60) {
if currentResidual < 0 {
// 上中天仍在地平线下:当日无升起,也就无落下。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, false, ErrNeverRise)
originalTimeZone, timeZone, zenithShift, height, false, ErrNeverRise, inst)
}
checkTime := estimatedTime + 12.0/24.0 + 6.0/15.0/24.0
angleSubtraction := 180 - MoonTimeAngle(checkTime, longitude, latitude, timeZone)
angleSubtraction := 180 - MoonTimeAngleWithInstance(checkTime, longitude, latitude, timeZone, inst)
checkTime += angleSubtraction * 4.0 / 60.0 / 24.0
if moonRiseSetResidual(checkTime, longitude, latitude, timeZone, zenithShift, height, -1) > 0 {
if moonRiseSetResidual(checkTime, longitude, latitude, timeZone, zenithShift, height, -1, inst) > 0 {
// 下中天仍在地平线上:当日无落下。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, false, ErrNeverSet)
originalTimeZone, timeZone, zenithShift, height, false, ErrNeverSet, inst)
}
}
moonDeclination := MoonApparentDec(estimatedTime, longitude, latitude, timeZone)
moonDeclination := MoonApparentDecWithInstance(estimatedTime, longitude, latitude, timeZone, inst)
tmp := (Sin(moonAngle) - Sin(moonDeclination)*Sin(latitude)) / (Cos(moonDeclination) * Cos(latitude))
if math.Abs(tmp) <= 1 && latitude < 85 {
@@ -428,7 +438,7 @@ func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, heigh
estimatedTime += hourAngle/24 + hourAngle/33.0/15.0
} else {
i := 0
for moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1) > 0 {
for moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1, inst) > 0 {
i++
estimatedTime += 15.0 / 60.0 / 24.0
if i > 48 {
@@ -438,10 +448,10 @@ func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, heigh
}
// 使用牛顿迭代法求精确解
estimatedTime, slope := moonRiseSetResidualIteration(estimatedTime, longitude, latitude, timeZone, zenithShift, height, 0.00002)
estimatedTime, slope := moonRiseSetResidualIteration(estimatedTime, longitude, latitude, timeZone, zenithShift, height, 0.00002, inst)
estimatedTime = estimatedTime - timeZone/24 + originalTimeZone/24
return moonRiseSetOnCivilDay(estimatedTime, slope, civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, false, nil)
originalTimeZone, timeZone, zenithShift, height, false, nil, inst)
}
// heightFunction 高度函数类型定义,用于牛顿迭代法
@@ -468,14 +478,16 @@ func moonRiseSetNewtonRaphsonIteration(initialTime, longitude, latitude, timeZon
return currentTime
}
func moonRiseSetResidualIteration(initialTime, longitude, latitude, timeZone, zenithShift, height, tolerance float64) (float64, float64) {
func moonRiseSetResidualIteration(initialTime, longitude, latitude, timeZone, zenithShift, height, tolerance float64, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
const derivativeStep = 0.000005
slope := math.NaN()
currentTime, ok := eventNewtonRefine(initialTime, tolerance, func(previousTime float64) float64 {
functionValue := moonRiseSetResidual(previousTime, longitude, latitude, timeZone, zenithShift, height, -1)
slope = (moonRiseSetResidual(previousTime+derivativeStep, longitude, latitude, timeZone, zenithShift, height, -1) -
moonRiseSetResidual(previousTime-derivativeStep, longitude, latitude, timeZone, zenithShift, height, -1)) / (2 * derivativeStep)
functionValue := moonRiseSetResidual(previousTime, longitude, latitude, timeZone, zenithShift, height, -1, inst)
slope = (moonRiseSetResidual(previousTime+derivativeStep, longitude, latitude, timeZone, zenithShift, height, -1, inst) -
moonRiseSetResidual(previousTime-derivativeStep, longitude, latitude, timeZone, zenithShift, height, -1, inst)) / (2 * derivativeStep)
return functionValue / slope
})
if !ok {
+12
View File
@@ -205,3 +205,15 @@ func CalcMoonX(year float64, quarterType int) float64 {
}
return jde
}
// solarEclipseAnchorNewMoon 种子所在朔望月的朔(TT);同一朔望月内任意种子返回逐位相同的结果。
func solarEclipseAnchorNewMoon(seedJDE float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
first := CalcMoonSHByJDEWithInstance(seedJDE, 0, inst)
if math.IsNaN(first) {
return first
}
k := math.Round((first - 2451550.09765) / 29.530588853)
return CalcMoonSHByJDEWithInstance(2451550.09765+29.530588853*k, 0, inst)
}
+115
View File
@@ -0,0 +1,115 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
func sunMoonSeekWithInstance(jde, degree float64, inst *Instance) float64 {
if inst == nil {
return SunMoonSeek(jde, degree)
}
p := HMoonApparentLoWithInstance(jde, inst) - HSunApparentLoWithInstance(jde, inst) - degree
for p < -180 {
p += 360
}
for p > 180 {
p -= 360
}
return p
}
func refineSunMoonPhaseWithInstance(jde, degree float64, inst *Instance) float64 {
estimateJD, ok := eventNewtonRefine(jde, 0.00001, func(prevJD float64) float64 {
value := sunMoonSeekWithInstance(prevJD, degree, inst)
slope := (sunMoonSeekWithInstance(prevJD+0.000005, degree, inst) - sunMoonSeekWithInstance(prevJD-0.000005, degree, inst)) / 0.00001
return value / slope
})
if !ok {
return math.NaN()
}
return estimateJD
}
// MoonPhaseWithInstance 月球照明比例 / illuminated lunar fraction with an explicit ephemeris instance.
func MoonPhaseWithInstance(jde float64, inst *Instance) float64 {
if inst == nil {
return MoonPhase(jde)
}
moonBo := HMoonTrueBoWithInstance(jde, inst)
sunLo := HSunApparentLoWithInstance(jde, inst)
moonLo := HMoonApparentLoWithInstance(jde, inst)
tmp := Cos(moonBo) * Cos(sunLo-moonLo)
earthSunDistance := EarthAwayNWithInstance(jde, -1, inst) * astronomicalUnitKM
i := earthSunDistance * Sin(ArcCos(tmp)) / (HMoonAwayWithInstance(jde, inst) - earthSunDistance*tmp)
i = ArcTan(i)
if i < 0 {
i += 180
}
if i > 180 {
i -= 180
}
return (1 + Cos(i)) / 2
}
// SunMoonSeekWithInstance 日月黄经差相位残差 / Sun-Moon longitude phase residual with an explicit ephemeris instance.
func SunMoonSeekWithInstance(jde, degree float64, inst *Instance) float64 {
return sunMoonSeekWithInstance(jde, degree, inst)
}
// CalcMoonSHByJDEWithInstance 按历元求朔望时刻 / calculate a syzygy from an epoch with an explicit ephemeris instance.
func CalcMoonSHByJDEWithInstance(jde float64, phaseType int, inst *Instance) float64 {
if inst == nil {
return CalcMoonSHByJDE(jde, phaseType)
}
return refineSunMoonPhaseWithInstance(jde, float64(phaseType*180), inst)
}
// CalcMoonSHWithInstance 按年份求朔望时刻 / calculate a syzygy from a year with an explicit ephemeris instance.
func CalcMoonSHWithInstance(year float64, phaseType int, inst *Instance) float64 {
if inst == nil {
return CalcMoonSH(year, phaseType)
}
return refineSunMoonPhaseWithInstance(CalcMoonSWithInstance(year, phaseType, inst), float64(phaseType*180), inst)
}
// CalcMoonSWithInstance 按年份估算朔望基准时刻 / estimate a syzygy base time from a year with an explicit ephemeris instance.
func CalcMoonSWithInstance(year float64, phaseType int, inst *Instance) float64 {
if inst == nil {
return CalcMoonS(year, phaseType)
}
return CalcMoonS(year, phaseType)
}
// CalcMoonXHByJDEWithInstance 按历元求弦月时刻 / calculate a quarter phase from an epoch with an explicit ephemeris instance.
func CalcMoonXHByJDEWithInstance(jde float64, quarterType int, inst *Instance) float64 {
if inst == nil {
return CalcMoonXHByJDE(jde, quarterType)
}
degree := -90.0
if quarterType == 0 {
degree = 90
}
return refineSunMoonPhaseWithInstance(jde, degree, inst)
}
// CalcMoonXHWithInstance 按年份求弦月时刻 / calculate a quarter phase from a year with an explicit ephemeris instance.
func CalcMoonXHWithInstance(year float64, quarterType int, inst *Instance) float64 {
if inst == nil {
return CalcMoonXH(year, quarterType)
}
degree := -90.0
if quarterType == 0 {
degree = 90
}
return refineSunMoonPhaseWithInstance(CalcMoonXWithInstance(year, quarterType, inst), degree, inst)
}
// CalcMoonXWithInstance 按年份估算弦月基准时刻 / estimate a quarter-phase base time from a year with an explicit ephemeris instance.
func CalcMoonXWithInstance(year float64, quarterType int, inst *Instance) float64 {
if inst == nil {
return CalcMoonX(year, quarterType)
}
return CalcMoonX(year, quarterType)
}
+16 -13
View File
@@ -27,14 +27,15 @@ type MoonPhysicalInfo struct {
PositionAngle float64
}
// MoonPhysical 月球物理观测参数 / physical observing parameters of the Moon.
func MoonPhysical(jde float64) MoonPhysicalInfo {
return MoonPhysicalN(jde, -1)
// MoonPhysicalWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonPhysicalWithInstance(jde float64, inst *Instance) MoonPhysicalInfo {
return MoonPhysicalNWithInstance(jde, -1, inst)
}
// MoonPhysicalN 月球物理观测参数(截断版) / truncated physical observing parameters of the Moon.
func MoonPhysicalN(jde float64, n int) MoonPhysicalInfo {
return moonPhysicalNFromCoordinates(jde, n, HMoonApparentLoN(jde, n), HMoonTrueBoN(jde, n), HMoonTrueRaN(jde, n))
// MoonPhysicalNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonPhysicalNWithInstance(jde float64, n int, inst *Instance) MoonPhysicalInfo {
lo, bo := moonApparentLoBoN(jde, n, inst)
return moonPhysicalNFromCoordinates(jde, n, lo, bo, HMoonTrueRaNWithInstance(jde, n, inst))
}
// MoonTopocentricPhysical 月球站心物理观测参数 / topocentric physical observing parameters of the Moon.
@@ -42,9 +43,9 @@ func MoonTopocentricPhysical(jde, observerLon, observerLat, height float64) Moon
return MoonTopocentricPhysicalN(jde, observerLon, observerLat, height, -1)
}
// MoonTopocentricPhysicalN 月球站心物理观测参数(截断版) / truncated topocentric physical observing parameters of the Moon.
func MoonTopocentricPhysicalN(jde, observerLon, observerLat, height float64, n int) MoonPhysicalInfo {
lambda, beta, alpha := moonTopocentricPhysicalCoordinatesN(jde, observerLon, observerLat, height, n)
// MoonTopocentricPhysicalNWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonTopocentricPhysicalNWithInstance(jde, observerLon, observerLat, height float64, n int, inst *Instance) MoonPhysicalInfo {
lambda, beta, alpha := moonTopocentricPhysicalCoordinatesN(jde, observerLon, observerLat, height, n, inst)
return moonPhysicalNFromCoordinates(jde, n, lambda, beta, alpha)
}
@@ -91,10 +92,12 @@ func moonPhysicalNFromCoordinates(jde float64, n int, lambda, beta, alpha float6
}
}
func moonTopocentricPhysicalCoordinatesN(jde, observerLon, observerLat, height float64, n int) (lambda, beta, alpha float64) {
geocentricRA := HMoonTrueRaN(jde, n)
geocentricDec := HMoonTrueDecN(jde, n)
distanceAU := HMoonAwayN(jde, n) / moonPhysicalAstronomicalUnitKM
func moonTopocentricPhysicalCoordinatesN(jde, observerLon, observerLat, height float64, n int, instances ...*Instance) (lambda, beta, alpha float64) {
inst := instanceValue(instances)
geocentricRA := HMoonTrueRaNWithInstance(jde, n, inst)
geocentricDec := HMoonTrueDecNWithInstance(jde, n, inst)
distanceAU := HMoonAwayNWithInstance(jde, n, inst) / moonPhysicalAstronomicalUnitKM
utcJD := TT2UTC(jde)
var topocentricDec float64
+77 -59
View File
@@ -1,6 +1,10 @@
package basic
import "math"
import (
"math"
"b612.me/astro/internal/geodata"
)
const (
moonPlanetConjunctionEstimateN = 8
@@ -42,36 +46,30 @@ const (
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, instances ...*Instance) float64 {
inst := instanceValue(instances)
func moonPlanetConjunctionDeltaAt(jdTT float64, planet MoonPlanetConjunctionPlanet, n int) float64 {
moonRA := HMoonGeocentricApparentRaN(jdTT, n)
moonRA := HMoonGeocentricApparentRaNWithInstance(jdTT, n, inst)
var planetRA float64
switch planet {
case MoonPlanetConjunctionMercury:
planetRA = MercuryApparentRaN(jdTT, n)
planetRA = MercuryApparentRaNWithInstance(jdTT, n, inst)
case MoonPlanetConjunctionVenus:
planetRA = VenusApparentRaN(jdTT, n)
planetRA = VenusApparentRaNWithInstance(jdTT, n, inst)
case MoonPlanetConjunctionMars:
planetRA = MarsApparentRaN(jdTT, n)
planetRA = MarsApparentRaNWithInstance(jdTT, n, inst)
case MoonPlanetConjunctionJupiter:
planetRA = JupiterApparentRaN(jdTT, n)
planetRA = JupiterApparentRaNWithInstance(jdTT, n, inst)
case MoonPlanetConjunctionSaturn:
planetRA = SaturnApparentRaN(jdTT, n)
planetRA = SaturnApparentRaNWithInstance(jdTT, n, inst)
case MoonPlanetConjunctionUranus:
planetRA = UranusApparentRaN(jdTT, n)
planetRA = UranusApparentRaNWithInstance(jdTT, n, inst)
case MoonPlanetConjunctionNeptune:
planetRA = NeptuneApparentRaN(jdTT, n)
planetRA = NeptuneApparentRaNWithInstance(jdTT, n, inst)
default:
return math.NaN()
}
return moonPlanetConjunctionWrappedDelta(moonRA - planetRA)
return geodata.NormalizeLongitude(moonRA - planetRA)
}
func moonPlanetConjunctionPeriodDays(planet MoonPlanetConjunctionPlanet) float64 {
@@ -114,7 +112,9 @@ func moonPlanetConjunctionInDirection(eventUT, queryTT float64, direction int) b
}
}
func moonPlanetConjunctionFindBracket(centerTT, halfSpan, step float64, planet MoonPlanetConjunctionPlanet) (float64, float64, bool) {
func moonPlanetConjunctionFindBracket(centerTT, halfSpan, step float64, planet MoonPlanetConjunctionPlanet, instances ...*Instance) (float64, float64, bool) {
inst := instanceValue(instances)
if math.IsNaN(centerTT) || math.IsNaN(halfSpan) || math.IsNaN(step) || halfSpan <= 0 || step <= 0 {
return 0, 0, false
}
@@ -122,7 +122,7 @@ func moonPlanetConjunctionFindBracket(centerTT, halfSpan, step float64, planet M
end := centerTT + halfSpan
samples := int(math.Ceil((end-start)/step)) + 1
prevTT := start
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1)
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1, inst)
if math.IsNaN(prevVal) {
return 0, 0, false
}
@@ -137,7 +137,7 @@ func moonPlanetConjunctionFindBracket(centerTT, halfSpan, step float64, planet M
if tt > end {
tt = end
}
val := moonPlanetConjunctionDeltaAt(tt, planet, -1)
val := moonPlanetConjunctionDeltaAt(tt, planet, -1, inst)
if math.IsNaN(val) {
return 0, 0, false
}
@@ -165,7 +165,9 @@ func moonPlanetConjunctionFindBracket(centerTT, halfSpan, step float64, planet M
return bestLeft, bestRight, true
}
func moonPlanetConjunctionRefineBracket(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet) float64 {
func moonPlanetConjunctionRefineBracket(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet, instances ...*Instance) float64 {
inst := instanceValue(instances)
if leftTT > rightTT {
leftTT, rightTT = rightTT, leftTT
}
@@ -175,13 +177,15 @@ func moonPlanetConjunctionRefineBracket(leftTT, rightTT float64, planet MoonPlan
center := (leftTT + rightTT) / 2.0
halfWindow := (rightTT - leftTT) / 2.0
return eventZeroRefine(center, halfWindow, moonPlanetConjunctionRefineStepDays, func(sampleTT float64) float64 {
return moonPlanetConjunctionDeltaAt(sampleTT, planet, -1)
return moonPlanetConjunctionDeltaAt(sampleTT, planet, -1, inst)
})
}
func moonPlanetConjunctionEventUT(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet) float64 {
eventTT := moonPlanetConjunctionRefineBracket(leftTT, rightTT, planet)
if math.Abs(moonPlanetConjunctionDeltaAt(eventTT, planet, -1)) > moonPlanetConjunctionEventTolerance {
func moonPlanetConjunctionEventUT(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet, instances ...*Instance) float64 {
inst := instanceValue(instances)
eventTT := moonPlanetConjunctionRefineBracket(leftTT, rightTT, planet, inst)
if math.Abs(moonPlanetConjunctionDeltaAt(eventTT, planet, -1, inst)) > moonPlanetConjunctionEventTolerance {
return math.NaN()
}
return TT2UTC(eventTT)
@@ -203,21 +207,25 @@ func moonPlanetConjunctionCollectLocalEvent(result *moonPlanetConjunctionLocalRe
}
}
func moonPlanetConjunctionShouldCheckLocal(queryTT float64, planet MoonPlanetConjunctionPlanet) bool {
delta := moonPlanetConjunctionDeltaAt(queryTT, planet, moonPlanetConjunctionEstimateN)
func moonPlanetConjunctionShouldCheckLocal(queryTT float64, planet MoonPlanetConjunctionPlanet, instances ...*Instance) bool {
inst := instanceValue(instances)
delta := moonPlanetConjunctionDeltaAt(queryTT, planet, moonPlanetConjunctionEstimateN, inst)
if math.IsNaN(delta) {
return false
}
return math.Abs(delta) <= moonPlanetConjunctionNearQueryDeltaDeg
}
func moonPlanetConjunctionLocalEvents(queryTT float64, planet MoonPlanetConjunctionPlanet) moonPlanetConjunctionLocalResult {
func moonPlanetConjunctionLocalEvents(queryTT float64, planet MoonPlanetConjunctionPlanet, instances ...*Instance) moonPlanetConjunctionLocalResult {
inst := instanceValue(instances)
result := emptyMoonPlanetConjunctionLocalResult()
start := queryTT - moonPlanetConjunctionNearQueryHalfSpan
end := queryTT + moonPlanetConjunctionNearQueryHalfSpan
step := moonPlanetConjunctionNearQueryStepDays
prevTT := start
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1)
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1, inst)
if math.IsNaN(prevVal) {
return result
}
@@ -227,12 +235,12 @@ func moonPlanetConjunctionLocalEvents(queryTT float64, planet MoonPlanetConjunct
if tt > end {
tt = end
}
val := moonPlanetConjunctionDeltaAt(tt, planet, -1)
val := moonPlanetConjunctionDeltaAt(tt, planet, -1, inst)
if math.IsNaN(val) {
return emptyMoonPlanetConjunctionLocalResult()
}
if prevVal == 0 || val == 0 || prevVal*val < 0 {
moonPlanetConjunctionCollectLocalEvent(&result, queryTT, moonPlanetConjunctionEventUT(prevTT, tt, planet))
moonPlanetConjunctionCollectLocalEvent(&result, queryTT, moonPlanetConjunctionEventUT(prevTT, tt, planet, inst))
}
if tt == end {
break
@@ -243,15 +251,19 @@ func moonPlanetConjunctionLocalEvents(queryTT float64, planet MoonPlanetConjunct
return result
}
func moonPlanetConjunctionMaybeLocalEvents(queryTT float64, planet MoonPlanetConjunctionPlanet) moonPlanetConjunctionLocalResult {
if !moonPlanetConjunctionShouldCheckLocal(queryTT, planet) {
func moonPlanetConjunctionMaybeLocalEvents(queryTT float64, planet MoonPlanetConjunctionPlanet, instances ...*Instance) moonPlanetConjunctionLocalResult {
inst := instanceValue(instances)
if !moonPlanetConjunctionShouldCheckLocal(queryTT, planet, inst) {
return emptyMoonPlanetConjunctionLocalResult()
}
return moonPlanetConjunctionLocalEvents(queryTT, planet)
return moonPlanetConjunctionLocalEvents(queryTT, planet, inst)
}
func moonPlanetConjunctionGuessTT(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 {
delta := moonPlanetConjunctionDeltaAt(queryTT, planet, moonPlanetConjunctionEstimateN)
func moonPlanetConjunctionGuessTT(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int, instances ...*Instance) float64 {
inst := instanceValue(instances)
delta := moonPlanetConjunctionDeltaAt(queryTT, planet, moonPlanetConjunctionEstimateN, inst)
if math.IsNaN(delta) {
return math.NaN()
}
@@ -269,7 +281,9 @@ func moonPlanetConjunctionGuessTT(queryTT float64, planet MoonPlanetConjunctionP
}
}
func moonPlanetConjunctionDirectionalFallback(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 {
func moonPlanetConjunctionDirectionalFallback(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int, instances ...*Instance) float64 {
inst := instanceValue(instances)
period := moonPlanetConjunctionPeriodDays(planet)
if math.IsNaN(period) {
return math.NaN()
@@ -291,7 +305,7 @@ func moonPlanetConjunctionDirectionalFallback(queryTT float64, planet MoonPlanet
}
prevTT := start
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1)
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1, inst)
if math.IsNaN(prevVal) {
return math.NaN()
}
@@ -301,12 +315,12 @@ func moonPlanetConjunctionDirectionalFallback(queryTT float64, planet MoonPlanet
if tt > end {
tt = end
}
val := moonPlanetConjunctionDeltaAt(tt, planet, -1)
val := moonPlanetConjunctionDeltaAt(tt, planet, -1, inst)
if math.IsNaN(val) {
return math.NaN()
}
if prevVal == 0 || val == 0 || prevVal*val < 0 {
eventUT := moonPlanetConjunctionEventUT(prevTT, tt, planet)
eventUT := moonPlanetConjunctionEventUT(prevTT, tt, planet, inst)
if !math.IsNaN(eventUT) && moonPlanetConjunctionInDirection(eventUT, queryTT, direction) {
if direction == 1 {
return eventUT
@@ -323,7 +337,9 @@ func moonPlanetConjunctionDirectionalFallback(queryTT float64, planet MoonPlanet
return bestEventUT
}
func moonPlanetConjunctionDirectionalEventWithLocal(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int, local moonPlanetConjunctionLocalResult) float64 {
func moonPlanetConjunctionDirectionalEventWithLocal(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int, local moonPlanetConjunctionLocalResult, instances ...*Instance) float64 {
inst := instanceValue(instances)
switch direction {
case -1:
if !math.IsNaN(local.lastUT) {
@@ -334,15 +350,15 @@ func moonPlanetConjunctionDirectionalEventWithLocal(queryTT float64, planet Moon
return local.nextUT
}
}
guessTT := moonPlanetConjunctionGuessTT(queryTT, planet, direction)
guessTT := moonPlanetConjunctionGuessTT(queryTT, planet, direction, inst)
if math.IsNaN(guessTT) {
return math.NaN()
}
halfSpan := moonPlanetConjunctionBracketHalfSpan
for attempt := 0; attempt < moonPlanetConjunctionBracketAttempts; attempt++ {
left, right, ok := moonPlanetConjunctionFindBracket(guessTT, halfSpan, moonPlanetConjunctionBracketStepDays, planet)
left, right, ok := moonPlanetConjunctionFindBracket(guessTT, halfSpan, moonPlanetConjunctionBracketStepDays, planet, inst)
if ok {
eventUT := moonPlanetConjunctionEventUT(left, right, planet)
eventUT := moonPlanetConjunctionEventUT(left, right, planet, inst)
if math.IsNaN(eventUT) {
halfSpan *= moonPlanetConjunctionBracketGrowth
continue
@@ -353,26 +369,28 @@ func moonPlanetConjunctionDirectionalEventWithLocal(queryTT float64, planet Moon
}
halfSpan *= moonPlanetConjunctionBracketGrowth
}
return moonPlanetConjunctionDirectionalFallback(queryTT, planet, direction)
return moonPlanetConjunctionDirectionalFallback(queryTT, planet, direction, inst)
}
func moonPlanetConjunctionDirectionalEvent(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 {
return moonPlanetConjunctionDirectionalEventWithLocal(queryTT, planet, direction, moonPlanetConjunctionMaybeLocalEvents(queryTT, planet))
func moonPlanetConjunctionDirectionalEvent(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int, instances ...*Instance) float64 {
inst := instanceValue(instances)
return moonPlanetConjunctionDirectionalEventWithLocal(queryTT, planet, direction, moonPlanetConjunctionMaybeLocalEvents(queryTT, planet, inst), inst)
}
// LastMoonPlanetConjunction 指定时刻之前最近一次行星合月(赤经合) / previous Moon-planet conjunction at or before jde.
func LastMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 {
return moonPlanetConjunctionDirectionalEvent(jde, planet, -1)
// LastMoonPlanetConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMoonPlanetConjunctionWithInstance(jde float64, planet MoonPlanetConjunctionPlanet, inst *Instance) float64 {
return moonPlanetConjunctionDirectionalEvent(jde, planet, -1, inst)
}
// NextMoonPlanetConjunction 指定时刻之后最近一次行星合月(赤经合) / next Moon-planet conjunction at or after jde.
func NextMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 {
return moonPlanetConjunctionDirectionalEvent(jde, planet, 1)
// NextMoonPlanetConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMoonPlanetConjunctionWithInstance(jde float64, planet MoonPlanetConjunctionPlanet, inst *Instance) float64 {
return moonPlanetConjunctionDirectionalEvent(jde, planet, 1, inst)
}
// ClosestMoonPlanetConjunction 离指定时刻最近一次行星合月(赤经合) / closest Moon-planet conjunction to jde.
func ClosestMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 {
local := moonPlanetConjunctionMaybeLocalEvents(jde, planet)
// ClosestMoonPlanetConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func ClosestMoonPlanetConjunctionWithInstance(jde float64, planet MoonPlanetConjunctionPlanet, inst *Instance) float64 {
local := moonPlanetConjunctionMaybeLocalEvents(jde, planet, inst)
if !math.IsNaN(local.lastUT) && !math.IsNaN(local.nextUT) {
if sameEventJD(local.lastUT, local.nextUT) {
return local.lastUT
@@ -385,8 +403,8 @@ func ClosestMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlane
if !math.IsNaN(local.nextUT) {
return local.nextUT
}
last := moonPlanetConjunctionDirectionalEventWithLocal(jde, planet, -1, local)
next := moonPlanetConjunctionDirectionalEventWithLocal(jde, planet, 1, local)
last := moonPlanetConjunctionDirectionalEventWithLocal(jde, planet, -1, local, inst)
next := moonPlanetConjunctionDirectionalEventWithLocal(jde, planet, 1, local, inst)
if math.IsNaN(last) {
return next
}
+70 -173
View File
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -3,9 +3,12 @@ package basic
import (
"math"
"testing"
"b612.me/astro/internal/ephemtest"
)
func TestMoonSeriesNumericalPrecision(t *testing.T) {
ephemtest.PinLegacy(t)
// The existing binary64 coefficients evaluated at 70 decimal digits.
// These check arithmetic precision, not the lunar theory's physical error.
for _, sample := range []struct{ jd, longitude, latitude, distance, velocity float64 }{
@@ -65,8 +68,7 @@ func TestOccultationContactDerivativeNumericalStability(t *testing.T) {
}
}
// MoonCalcNew 与 HMoonTrueLo 是同一个物理量的两个入口,必须给出同一个值(此前只有一个入口
// 走线性相位补偿,两者差约 1e-9 度)。
// MoonCalcNew 与 HMoonTrueLo 是同一个物理量的两个入口,必须给出同一个值。
func TestMoonCalcNewMatchesHMoonTrueLo(t *testing.T) {
for _, jd := range []float64{2451545.0, 2460310.5, 2415020.5, 2299160.5, 2500000.5} {
direct := math.Mod(MoonCalcNew(0, jd)*180/math.Pi, 360)
+4 -3
View File
@@ -197,11 +197,12 @@ func (engine movingDiskEventEngine) sampleTimes(
if step <= 0 || math.IsNaN(step) || math.IsInf(step, 0) {
step = duration
}
baseSampleCount := int(math.Ceil(duration/step)) + 1
// 先按浮点比较,避免极小步长下 int 转换溢出成负数而绕过上限。
baseSampleCount := math.Ceil(duration/step) + 1
if engine.reserveAnchorSlot {
baseSampleCount++
}
if baseSampleCount > maximum && engine.uniformOverflow {
if baseSampleCount > float64(maximum) && engine.uniformOverflow {
interiorCount := maximum - 3
if interiorCount < 0 {
interiorCount = 0
@@ -214,7 +215,7 @@ func (engine movingDiskEventEngine) sampleTimes(
sort.Float64s(bounded)
return movingDiskUniqueTimes(bounded), step
}
if baseSampleCount > maximum {
if baseSampleCount > float64(maximum) {
step = duration / float64(maximum-1)
}
+10
View File
@@ -182,3 +182,13 @@ func TestMovingDiskContactCacheClearsAtBoundedCapacity(t *testing.T) {
t.Fatal("new entry missing after bounded cache clear")
}
}
func TestMovingDiskEventEngineSampleTimesBoundsTinyStep(t *testing.T) {
engine := movingDiskEventEngine{maxSampleCount: 9}
for _, step := range []float64{1e-300, math.SmallestNonzeroFloat64} {
times, _ := engine.sampleTimes(0, 1, 0.5, step)
if len(times) > 9 {
t.Fatalf("step %g: sample count = %d, want <= 9", step, len(times))
}
}
}
+27 -44
View File
@@ -115,59 +115,34 @@ func NeptuneMag(jde float64) float64 {
return FloatRound(mag, 2)
}
func NeptuneHeight(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := NeptuneApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式
// sin(h)=sin(lat)*sin(dec)+cos(dec)*cos(lat)*cos(hourAngle)
sinHeight := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(hourAngle)
return ArcSin(sinHeight)
// NeptuneHeightWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NeptuneHeightWithInstance(localJD, lon, lat, timezone float64, inst *Instance) float64 {
return planetHeightN(7, localJD, lon, lat, timezone, -1, inst)
}
func NeptuneAzimuth(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时
utcJD := localJD - timezone/24.0
// 计算视恒星时
ra, dec := NeptuneApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角
hourAngle := Limit360(st - ra)
// 三角转换公式
tanAzimuth := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(dec)*Cos(lat))
azimuth := ArcTan(tanAzimuth)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
}
return azimuth + 180
}
if hourAngle/15 < 12 {
return azimuth + 180
}
return azimuth
// NeptuneAzimuthWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NeptuneAzimuthWithInstance(localJD, lon, lat, timezone float64, inst *Instance) float64 {
return planetAzimuthN(7, localJD, lon, lat, timezone, -1, inst)
}
func NeptuneHourAngle(jd, lon, timezone float64) float64 {
// NeptuneHourAngleWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NeptuneHourAngleWithInstance(jd, lon, timezone float64, inst *Instance) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - NeptuneApparentRa(UTC2TT(jd-timezone/24.0))
hourAngle := siderealLongitude - NeptuneApparentRaWithInstance(UTC2TT(jd-timezone/24.0), inst)
if hourAngle < 0 {
hourAngle += 360
}
return hourAngle
}
func NeptuneCulminationTime(localJD, lon, timezone float64) float64 {
// NeptuneCulminationTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NeptuneCulminationTimeWithInstance(localJD, lon, timezone float64, inst *Instance) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-NeptuneHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
estimateJD := localJD + Limit360(360-NeptuneHourAngleWithInstance(localJD, lon, timezone, inst))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := NeptuneHourAngle(localJD, lon, timezone)
currentHourAngle := NeptuneHourAngleWithInstance(localJD, lon, timezone, inst)
if currentHourAngle < 180 {
currentHourAngle += 360
}
@@ -185,14 +160,22 @@ func NeptuneCulminationTime(localJD, lon, timezone float64) float64 {
return estimateJD
}
func NeptuneRiseTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return neptuneRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true)
// NeptuneRiseTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NeptuneRiseTimeWithInstance(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, inst *Instance) (float64, error) {
return neptuneRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, true, inst)
}
func NeptuneSetTime(jd, lon, lat, timezone, aeroCorrection, observerHeight float64) (float64, error) {
return neptuneRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false)
// NeptuneSetTimeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NeptuneSetTimeWithInstance(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, inst *Instance) (float64, error) {
return neptuneRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, false, inst)
}
func neptuneRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool) (float64, error) {
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, NeptuneCulminationTime, NeptuneHeight, NeptuneApparentDec)
func neptuneRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool, instances ...*Instance) (float64, error) {
inst := instanceValue(instances)
return planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight, isRise, func(localJD, lon, timezone float64) float64 {
return NeptuneCulminationTimeWithInstance(localJD, lon, timezone, inst)
}, func(localJD, lon, lat, timezone float64) float64 {
return NeptuneHeightWithInstance(localJD, lon, lat, timezone, inst)
}, func(jde float64) float64 { return NeptuneApparentDecWithInstance(jde, inst) })
}
+101 -59
View File
@@ -14,8 +14,10 @@ const (
neptunePhaseCoarseTolerance = 30.0 / 86400.0
)
func neptuneSunLongitudeDelta(jde, degree float64, filter bool) float64 {
sub := Limit360(Limit360(NeptuneApparentLo(jde)-HSunApparentLo(jde)) - degree)
func neptuneSunLongitudeDelta(jde, degree float64, filter bool, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := Limit360(Limit360(NeptuneApparentLoWithInstance(jde, inst)-HSunApparentLoWithInstance(jde, inst)) - degree)
if filter {
if sub > 180 {
sub -= 360
@@ -27,8 +29,10 @@ func neptuneSunLongitudeDelta(jde, degree float64, filter bool) float64 {
return sub
}
func neptuneSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64 {
sub := Limit360(Limit360(NeptuneApparentLoN(jde, n)-HSunApparentLoN(jde, n)) - degree)
func neptuneSunLongitudeDeltaN(jde, degree float64, filter bool, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := Limit360(Limit360(NeptuneApparentLoNWithInstance(jde, n, inst)-HSunApparentLoNWithInstance(jde, n, inst)) - degree)
if filter {
if sub > 180 {
sub -= 360
@@ -40,8 +44,10 @@ 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)
func neptuneRADerivative(jde, delta float64, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := NeptuneApparentRaWithInstance(jde+delta, inst) - NeptuneApparentRaWithInstance(jde-delta, inst)
if sub > 180 {
sub -= 360
}
@@ -51,8 +57,10 @@ func neptuneRADerivative(jde, delta float64) float64 {
return sub / (2 * delta)
}
func neptuneRADerivativeN(jde, delta float64, n int) float64 {
sub := NeptuneApparentRaN(jde+delta, n) - NeptuneApparentRaN(jde-delta, n)
func neptuneRADerivativeN(jde, delta float64, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
sub := NeptuneApparentRaNWithInstance(jde+delta, n, inst) - NeptuneApparentRaNWithInstance(jde-delta, n, inst)
if sub > 180 {
sub -= 360
}
@@ -62,13 +70,15 @@ func neptuneRADerivativeN(jde, delta float64, n int) float64 {
return sub / (2 * delta)
}
func neptuneConjunctionFull(jde, degree float64, next uint8) float64 {
func neptuneConjunctionFull(jde, degree float64, next uint8, instances ...*Instance) float64 {
inst := instanceValue(instances)
//0=last 1=next
if !isFiniteFloat(jde) || !isFiniteFloat(degree) {
return math.NaN()
}
daysPerDegree := NEPTUNE_S_PERIOD / 360
currentDelta := neptuneSunLongitudeDelta(jde, degree, false)
currentDelta := neptuneSunLongitudeDelta(jde, degree, false, inst)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
@@ -78,8 +88,8 @@ func neptuneConjunctionFull(jde, degree float64, next uint8) float64 {
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := neptuneSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (neptuneSunLongitudeDelta(prevJDE+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
longitudeDelta := neptuneSunLongitudeDelta(prevJDE, degree, true, inst)
longitudeSlope := (neptuneSunLongitudeDelta(prevJDE+0.000005, degree, true, inst) - neptuneSunLongitudeDelta(prevJDE-0.000005, degree, true, inst)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
@@ -93,13 +103,15 @@ func neptuneConjunctionFull(jde, degree float64, next uint8) float64 {
return TT2UTC(estimateJDE)
}
func neptuneConjunction(jde, degree float64, next uint8) float64 {
func neptuneConjunction(jde, degree float64, next uint8, instances ...*Instance) float64 {
inst := instanceValue(instances)
//0=last 1=next
if !isFiniteFloat(jde) || !isFiniteFloat(degree) {
return math.NaN()
}
daysPerDegree := NEPTUNE_S_PERIOD / 360
currentDelta := neptuneSunLongitudeDelta(jde, degree, false)
currentDelta := neptuneSunLongitudeDelta(jde, degree, false, inst)
if next == 0 {
jde -= (360 - currentDelta) * daysPerDegree
} else {
@@ -109,8 +121,8 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 {
converged := false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := neptuneSunLongitudeDeltaN(prevJDE, degree, true, neptuneEventSearchN)
longitudeSlope := (neptuneSunLongitudeDeltaN(prevJDE+0.000005, degree, true, neptuneEventSearchN) - neptuneSunLongitudeDeltaN(prevJDE-0.000005, degree, true, neptuneEventSearchN)) / 0.00001
longitudeDelta := neptuneSunLongitudeDeltaN(prevJDE, degree, true, neptuneEventSearchN, inst)
longitudeSlope := (neptuneSunLongitudeDeltaN(prevJDE+0.000005, degree, true, neptuneEventSearchN, inst) - neptuneSunLongitudeDeltaN(prevJDE-0.000005, degree, true, neptuneEventSearchN, inst)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= neptunePhaseCoarseTolerance {
@@ -124,8 +136,8 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 {
converged = false
for i := 0; i < eventNewtonMaxIterations; i++ {
prevJDE := estimateJDE
longitudeDelta := neptuneSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (neptuneSunLongitudeDelta(prevJDE+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
longitudeDelta := neptuneSunLongitudeDelta(prevJDE, degree, true, inst)
longitudeSlope := (neptuneSunLongitudeDelta(prevJDE+0.000005, degree, true, inst) - neptuneSunLongitudeDelta(prevJDE-0.000005, degree, true, inst)) / 0.00001
nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJDE = nextJD
if math.Abs(nextJD-prevJDE) <= 0.00001 {
@@ -139,39 +151,65 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 {
return TT2UTC(estimateJDE)
}
func LastNeptuneConjunction(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 0, neptuneConjunction)
// LastNeptuneConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastNeptuneConjunctionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 0, func(jde, degree float64, next uint8) float64 {
return neptuneConjunction(jde, degree, next, inst)
})
}
func NextNeptuneConjunction(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 0, neptuneConjunction)
// NextNeptuneConjunctionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextNeptuneConjunctionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 0, func(jde, degree float64, next uint8) float64 {
return neptuneConjunction(jde, degree, next, inst)
})
}
func LastNeptuneOpposition(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 180, neptuneConjunction)
// LastNeptuneOppositionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastNeptuneOppositionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 180, func(jde, degree float64, next uint8) float64 {
return neptuneConjunction(jde, degree, next, inst)
})
}
func NextNeptuneOpposition(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 180, neptuneConjunction)
// NextNeptuneOppositionWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextNeptuneOppositionWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 180, func(jde, degree float64, next uint8) float64 {
return neptuneConjunction(jde, degree, next, inst)
})
}
func NextNeptuneEasternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 90, neptuneConjunction)
// NextNeptuneEasternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextNeptuneEasternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 90, func(jde, degree float64, next uint8) float64 {
return neptuneConjunction(jde, degree, next, inst)
})
}
func LastNeptuneEasternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 90, neptuneConjunction)
// LastNeptuneEasternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastNeptuneEasternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 90, func(jde, degree float64, next uint8) float64 {
return neptuneConjunction(jde, degree, next, inst)
})
}
func NextNeptuneWesternQuadrature(jde float64) float64 {
return inclusiveNextPhaseEvent(jde, 270, neptuneConjunction)
// NextNeptuneWesternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextNeptuneWesternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveNextPhaseEvent(jde, 270, func(jde, degree float64, next uint8) float64 {
return neptuneConjunction(jde, degree, next, inst)
})
}
func LastNeptuneWesternQuadrature(jde float64) float64 {
return inclusiveLastPhaseEvent(jde, 270, neptuneConjunction)
// LastNeptuneWesternQuadratureWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastNeptuneWesternQuadratureWithInstance(jde float64, inst *Instance) float64 {
return inclusiveLastPhaseEvent(jde, 270, func(jde, degree float64, next uint8) float64 {
return neptuneConjunction(jde, degree, next, inst)
})
}
func neptuneRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
func neptuneRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool, instances ...*Instance) float64 {
inst := instanceValue(instances)
if !isFiniteFloat(oppositionJD) {
return math.NaN()
}
@@ -179,103 +217,107 @@ func neptuneRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposit
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
easternQuadratureUT := neptuneConjunction(oppositionTT, 90, 0)
easternQuadratureUT := neptuneConjunction(oppositionTT, 90, 0, inst)
startTT = UTC2TT(easternQuadratureUT)
} else {
westernQuadratureUT := neptuneConjunction(oppositionTT, 270, 1)
westernQuadratureUT := neptuneConjunction(oppositionTT, 270, 1, inst)
endTT = UTC2TT(westernQuadratureUT)
}
bestJDE := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return neptuneRADerivativeN(jd, stationDerivativeStepDay, neptuneEventSearchN)
return neptuneRADerivativeN(jd, stationDerivativeStepDay, neptuneEventSearchN, inst)
}, func(jd float64) float64 {
return neptuneRADerivative(jd, stationDerivativeStepDay)
return neptuneRADerivative(jd, stationDerivativeStepDay, inst)
})
return TT2UTC(bestJDE)
}
func NextNeptuneRetrogradeToPrograde(jde float64) float64 {
// NextNeptuneRetrogradeToProgradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextNeptuneRetrogradeToProgradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
date := neptuneRetrogradeAroundOpposition(lastOppositionJD, false)
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0, inst)
date := neptuneRetrogradeAroundOpposition(lastOppositionJD, false, inst)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1, inst)
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
date = neptuneRetrogradeAroundOpposition(nextOppositionJD, false)
date = neptuneRetrogradeAroundOpposition(nextOppositionJD, false, inst)
if !isFiniteFloat(date) || !stationUTQueryAfterOrEqual(date, jde) {
return math.NaN()
}
return date
}
func LastNeptuneRetrogradeToPrograde(jde float64) float64 {
// LastNeptuneRetrogradeToProgradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastNeptuneRetrogradeToProgradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
date := neptuneRetrogradeAroundOpposition(lastOppositionJD, false)
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0, inst)
date := neptuneRetrogradeAroundOpposition(lastOppositionJD, false, inst)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
if !isFiniteFloat(lastOppositionJD) {
return math.NaN()
}
previousOppositionJD := neptuneConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
previousOppositionJD := neptuneConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0, inst)
if !isFiniteFloat(previousOppositionJD) {
return math.NaN()
}
date = neptuneRetrogradeAroundOpposition(previousOppositionJD, false)
date = neptuneRetrogradeAroundOpposition(previousOppositionJD, false, inst)
if !isFiniteFloat(date) || !stationUTQueryBeforeOrEqual(date, jde) {
return math.NaN()
}
return date
}
func NextNeptuneProgradeToRetrograde(jde float64) float64 {
// NextNeptuneProgradeToRetrogradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextNeptuneProgradeToRetrogradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
date := neptuneRetrogradeAroundOpposition(nextOppositionJD, true)
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1, inst)
date := neptuneRetrogradeAroundOpposition(nextOppositionJD, true, inst)
if isFiniteFloat(date) && stationUTQueryAfterOrEqual(date, jde) {
return date
}
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
followingOppositionJD := neptuneConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
followingOppositionJD := neptuneConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1, inst)
if !isFiniteFloat(followingOppositionJD) {
return math.NaN()
}
date = neptuneRetrogradeAroundOpposition(followingOppositionJD, true)
date = neptuneRetrogradeAroundOpposition(followingOppositionJD, true, inst)
if !isFiniteFloat(date) || !stationUTQueryAfterOrEqual(date, jde) {
return math.NaN()
}
return date
}
func LastNeptuneProgradeToRetrograde(jde float64) float64 {
// LastNeptuneProgradeToRetrogradeWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastNeptuneProgradeToRetrogradeWithInstance(jde float64, inst *Instance) float64 {
if !isFiniteFloat(jde) {
return math.NaN()
}
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
date := neptuneRetrogradeAroundOpposition(nextOppositionJD, true)
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1, inst)
date := neptuneRetrogradeAroundOpposition(nextOppositionJD, true, inst)
if isFiniteFloat(date) && stationUTQueryBeforeOrEqual(date, jde) {
return date
}
if !isFiniteFloat(nextOppositionJD) {
return math.NaN()
}
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0, inst)
if !isFiniteFloat(lastOppositionJD) {
return math.NaN()
}
date = neptuneRetrogradeAroundOpposition(lastOppositionJD, true)
date = neptuneRetrogradeAroundOpposition(lastOppositionJD, true, inst)
if !isFiniteFloat(date) || !stationUTQueryBeforeOrEqual(date, jde) {
return math.NaN()
}
+25 -8
View File
@@ -55,9 +55,12 @@ const (
// OccultationPartial 表示掩甚时有限目标盘面只有部分被覆盖。
// OccultationPartial means only part of a finite target disk is covered at greatest occultation.
OccultationPartial OccultationType = "partial"
// OccultationGrazing 表示两边缘相切,且没有正持续时间的重叠。
// OccultationGrazing means the limbs are tangent without a positive-duration overlap.
// OccultationGrazing 表示掠掩:圆月面下两边缘相切、无正持续时间的重叠;按月缘剖面求解时指星迹始终在月缘起伏带内,可有多段遮挡。
// OccultationGrazing is a graze: with a circular limb the limbs are tangent without a positive-duration overlap; with a limb profile the track stays inside the relief band and may be hidden several times.
OccultationGrazing OccultationType = "grazing"
// OccultationNone 表示月缘剖面下恒星未被遮挡,只出现在精密剖面结果中。
// OccultationNone means the star is never hidden by the limb profile; it only appears in precise profile results.
OccultationNone OccultationType = "none"
)
// OccultationPlanet 标识有限盘面的行星目标。
@@ -153,9 +156,11 @@ type Observer struct {
// 通用 MoonSemidiameterN 使用地心距离;月掩接触使用同一站心视差修正后的观测者到月球距离。
// moonTopocentricSemidiameterN returns the lunar angular radius as seen from the supplied site.
// The usual MoonSemidiameterN uses geocentric distance; occultation contacts use the observer-to-Moon distance after the same topocentric parallax correction as the direction.
func moonTopocentricSemidiameterN(tt float64, observer Observer, n int) float64 {
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, n)
moonDistanceKM := HMoonAwayN(tt, n)
func moonTopocentricSemidiameterN(tt float64, observer Observer, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
moonRA, moonDec := HMoonGeocentricApparentRaDecNWithInstance(tt, n, inst)
moonDistanceKM := HMoonAwayNWithInstance(tt, n, inst)
if !finite(moonRA) || !finite(moonDec) || !finite(moonDistanceKM) || moonDistanceKM <= 0 {
return math.NaN()
}
@@ -179,7 +184,6 @@ func topocentricDistanceKMWithSidereal(
observer Observer,
siderealDegrees float64,
) float64 {
const earthEquatorialRadius = 6378.14
const astronomicalUnitKM = angularDiameterAstronomicalUnitKM
distanceAU := distanceKM / astronomicalUnitKM
@@ -194,7 +198,7 @@ func topocentricDistanceKMWithSidereal(
distanceAU * math.Sin(decRad),
}
theta := (siderealDegrees + observer.Longitude) * math.Pi / 180
observerAU := earthEquatorialRadius / astronomicalUnitKM
observerAU := topocentricEarthRadiusKM / astronomicalUnitKM
observerVector := [3]float64{
observerAU * pcosi(observer.Latitude, observer.Height) * math.Cos(theta),
observerAU * pcosi(observer.Latitude, observer.Height) * math.Sin(theta),
@@ -350,6 +354,9 @@ type OccultationSearchOptions struct {
// MaxEvents 为正时限制返回事件数量。
// MaxEvents limits the number of returned events when positive.
MaxEvents int
// LunarLimb 月缘剖面,nil 为圆月面。站心恒星、行星月掩按剖面重新判定是否成掩与食型(可新增或取消圆月面事件),接触取最外侧;
// 恒星多段遮挡只报首次掩入与最后复现,全部区间见 StarOccultationLimbProfile。全球路径只修正界线 / lunar limb profile; local events are re-decided on the profile.
LunarLimb LunarLimb
}
// OccultationPathAlgorithm 选择全球月掩路径的星历求解分支。
@@ -375,7 +382,9 @@ const (
type OccultationPathOptions struct {
// Algorithm 的零值等同 optimized;exact 可选择原有精确分支。此选项不影响独立的单时刻月影与事件查询接口。
// Algorithm defaults to optimized; exact selects the original branch. Independent instant-footprint and event-search APIs are unaffected.
Algorithm OccultationPathAlgorithm
Algorithm OccultationPathAlgorithm
// LunarLimb 月缘剖面,nil 为圆月面;只修正外接触南北界(掠掩线),掩带填充与等时线仍按圆月面 / lunar limb profile for graze lines only.
LunarLimb LunarLimb
Step time.Duration
TargetSpacingKM float64
// RiseSetStep 独立控制六类升落阶段线的采样步长;零值使用 5 分钟。
@@ -472,6 +481,8 @@ type StarOccultationInfo struct {
// ContactsComplete 表示两个月缘接触时刻均已求解。
// ContactsComplete is true when both lunar-limb contacts were solved.
ContactsComplete bool
// LunarLimbApplied 表示接触时刻已按月缘剖面精修,否则为圆月面 / contacts refined with a lunar limb profile.
LunarLimbApplied bool
MinimumSeparationArcsec float64
PositionAngleDeg float64
@@ -502,6 +513,8 @@ type PlanetOccultationInfo struct {
HasInternalContacts bool
ContactsComplete bool
// LunarLimbApplied 表示接触时刻已按月缘剖面精修,否则为圆月面 / contacts refined with a lunar limb profile.
LunarLimbApplied bool
MinimumSeparationArcsec float64
PositionAngleDeg float64
@@ -602,6 +615,8 @@ type StarOccultationPath struct {
// GreatestTimeContours 是按掩甚时刻采样的等时线。
// GreatestTimeContours are sampled local greatest-occultation time isolines.
GreatestTimeContours []OccultationGreatestTimeContour
// LunarLimbApplied 表示南北限已按月缘剖面修正,否则为圆月面 / limits corrected with a lunar limb profile.
LunarLimbApplied bool
Step time.Duration
TargetSpacingKM float64
@@ -733,6 +748,8 @@ type PlanetOccultationPath struct {
// GreatestTimeContours 是按掩甚时刻采样的等时线,判据用外接触锥,与偏掩可见域一致。
// GreatestTimeContours are sampled local greatest-occultation time isolines, gated by the outer-contact cone so they match the partial-occultation visibility area.
GreatestTimeContours []OccultationGreatestTimeContour
// LunarLimbApplied 表示南北限已按月缘剖面修正,否则为圆月面 / limits corrected with a lunar limb profile.
LunarLimbApplied bool
Step time.Duration
TargetSpacingKM float64
+7 -9
View File
@@ -44,9 +44,8 @@ type PlanetOccultationInstant struct {
Total *PlanetOccultationFootprint
}
// StarOccultationFootprintAt 返回指定时刻的精确点源恒星月掩可见足迹;它采用与路径时间线相同的分辨率,并对完整接触弧执行站心校正。
// StarOccultationFootprintAt returns the exact visible lunar-occultation footprint of a point-source star at one instant, using timeline resolution and station-centred correction of the complete contact arc.
func StarOccultationFootprintAt(at time.Time, star StarCoordinate) (StarOccultationInstant, error) {
// StarOccultationFootprintAtWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func StarOccultationFootprintAtWithInstance(at time.Time, star StarCoordinate, inst *Instance) (StarOccultationInstant, error) {
result := StarOccultationInstant{Time: at, TargetID: star.ID}
if at.IsZero() {
return result, fmt.Errorf("%w: time is required", ErrInvalidOccultationInput)
@@ -55,7 +54,7 @@ func StarOccultationFootprintAt(at time.Time, star StarCoordinate) (StarOccultat
return result, err
}
cache := newStarOccultationEventCache(star)
cache := newStarOccultationEventCache(star, inst)
tt := occultationTimeToTT(at)
result.DeltaTSeconds = DeltaT(tt, true)
result.SublunarLongitude, result.SublunarLatitude = occultationSublunarPoint(tt, cache.frameAt)
@@ -65,9 +64,8 @@ func StarOccultationFootprintAt(at time.Time, star StarCoordinate) (StarOccultat
return result, nil
}
// PlanetOccultationFootprintsAt 返回指定时刻有限行星盘面的精确外接触和内接触月掩可见足迹;它采用路径时间线分辨率,并对每条完整接触弧执行站心校正。
// PlanetOccultationFootprintsAt returns the exact visible outer- and inner-contact lunar-occultation footprints of a finite planetary disk at one instant, using timeline resolution and station-centred correction of each complete contact arc.
func PlanetOccultationFootprintsAt(at time.Time, planet OccultationPlanet) (PlanetOccultationInstant, error) {
// PlanetOccultationFootprintsAtWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func PlanetOccultationFootprintsAtWithInstance(at time.Time, planet OccultationPlanet, inst *Instance) (PlanetOccultationInstant, error) {
result := PlanetOccultationInstant{Time: at, Planet: planet, TargetID: planet.String()}
if at.IsZero() {
return result, fmt.Errorf("%w: time is required", ErrInvalidOccultationInput)
@@ -76,8 +74,8 @@ func PlanetOccultationFootprintsAt(at time.Time, planet OccultationPlanet) (Plan
return result, err
}
config, _ := planetOccultationConfigFor(planet)
cache := newPlanetOccultationEventCache(config)
config, _ := planetOccultationConfigFor(planet, inst)
cache := newPlanetOccultationEventCache(config, inst)
tt := occultationTimeToTT(at)
result.DeltaTSeconds = DeltaT(tt, true)
result.SublunarLongitude, result.SublunarLatitude = occultationSublunarPoint(tt, cache.outerFrameAt)
+4 -2
View File
@@ -3,6 +3,8 @@ package basic
import (
"math"
"time"
"b612.me/astro/internal/geodata"
)
const (
@@ -80,7 +82,7 @@ func (arc occultationGreatestTimeArc) residual(longitude, latitude float64) floa
func (arc occultationGreatestTimeArc) point(longitude, latitude float64, state occultationRiseSetState) OccultationPathPoint {
return OccultationPathPoint{
Time: occultationTTToLocation(arc.evaluation.tt, arc.location),
Longitude: normalizeLongitude(longitude),
Longitude: geodata.NormalizeLongitude(longitude),
Latitude: latitude,
MoonAltitude: state.moonAltitude,
}
@@ -219,7 +221,7 @@ func traceOccultationGreatestTimeArc(
next := arc.point(nextLongitude, nextLatitude, nextState)
distance := occultationPathDistanceKM(points[len(points)-1], next)
// 校正回到原点说明该方向已经走到支路端点,继续只会原地打转。
if distance < 1e-9 || distance > 4*step*greatestTimeContourKMPerDegree {
if distance < 1e-9 || distance > 4*step*geodata.EarthKMPerDegree {
break
}
points = append(points, next)
+360
View File
@@ -0,0 +1,360 @@
package basic
import (
"math"
"time"
)
// occultationLimbTarget 给出某时刻目标的地心视赤经、赤纬(度)与地心距(km,恒星为 0)及站心角半径(角秒)。
type occultationLimbTarget func(tt float64) (ra, dec, distanceKM, radiusArcsec float64, ok bool)
func planetOccultationLimbTarget(config planetOccultationConfig, observer Observer) occultationLimbTarget {
return func(tt float64) (float64, float64, float64, float64, bool) {
ra, dec := config.apparentRaDecN(tt, -1)
distanceKM := config.earthDistanceN(tt, -1) * angularDiameterAstronomicalUnitKM
radius := planetTopocentricSemidiameterN(tt, config, observer, -1)
return ra, dec, distanceKM, radius, finite(ra) && finite(dec) && finite(distanceKM) && finite(radius)
}
}
const (
occultationLimbCrossingStepDays = 60.0 / 86400.0
occultationLimbCrossingSpanDays = 3.5 / 24.0
occultationLimbRootDays = 1e-3 / 86400.0
occultationLimbMinSamples = 64
occultationLimbMaxSamples = 4000
occultationLimbBandPadArcsec = 0.05
occultationLimbUnrangedPadKM = 1.0
planetOccultationLimbStepDays = 0.25 / 86400.0
)
// occultationLimbRangeKM 返回剖面半径范围;剖面不报告范围时在 tt 时刻朝目标扫一圈并外扩 1 km。
func occultationLimbRangeKM(tt float64, limb LunarLimb, observer Observer, target occultationLimbTarget, inst *Instance) (float64, float64, bool) {
geoRA, geoDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
geoDistance := HMoonAwayNWithInstance(tt, -1, inst)
frame := newLunarLimbFrame(limb, tt, lunarEclipseLimbVector(geoRA, geoDec, geoDistance), inst)
if !frame.valid {
return 0, 0, false
}
if frame.ranged {
return frame.minKM, frame.maxKM, true
}
ra, dec, _, _, ok := target(tt)
if !ok {
return 0, 0, false
}
topoRA, topoDec := moonTopocentricApparentRaDec(tt, observer, -1, inst)
view, ok := frame.viewToward(lunarEclipseLimbVector(topoRA, topoDec, 1), lunarEclipseLimbVector(ra, dec, 1))
if !ok {
return 0, 0, false
}
low, high := math.Inf(1), math.Inf(-1)
for offset := 0.0; offset < 360; offset += frame.stepDeg {
radius, ok := frame.radiusAt(view, offset)
if !ok {
return 0, 0, false
}
low, high = math.Min(low, radius), math.Max(high, radius)
}
return low - occultationLimbUnrangedPadKM, high + occultationLimbUnrangedPadKM, true
}
// occultationLimbCrossing 自 greatestTT 向 direction 一侧求圆月面间隙升到 level 的时刻;间隙自掩甚单调增大,起点已不低于 level 时返回起点。
func occultationLimbCrossing(greatestTT, direction, level float64, circular func(float64) (float64, bool)) (float64, bool) {
near := greatestTT
value, ok := circular(near)
if !ok {
return 0, false
}
if value >= level {
return greatestTT, true
}
for step := 1; float64(step)*occultationLimbCrossingStepDays <= occultationLimbCrossingSpanDays; step++ {
far := greatestTT + direction*float64(step)*occultationLimbCrossingStepDays
farValue, ok := circular(far)
if !ok {
return 0, false
}
if farValue < level {
near = far
continue
}
for i := 0; i < 64 && math.Abs(far-near) > occultationLimbRootDays; i++ {
mid := (near + far) / 2
midValue, ok := circular(mid)
if !ok {
return 0, false
}
if midValue < level {
near = mid
} else {
far = mid
}
}
return (near + far) / 2, true
}
return 0, false
}
// occultationLimbOuterRoots 在 [start, end] 上采样月缘间隙,返回首个由正转负与最后一个由负转正的零点;
// 端点状态由调用方保证(hiddenAtStart、hiddenAtEnd),ok 为假表示剖面无值或星历失败。
func occultationLimbOuterRoots(start, end float64, hiddenAtStart, hiddenAtEnd bool, gap func(float64) (float64, bool)) (first, last float64, found, ok bool) {
if end <= start {
return 0, 0, false, true
}
count := int(math.Ceil((end - start) / planetOccultationLimbStepDays))
if count < occultationLimbMinSamples {
count = occultationLimbMinSamples
}
if count > occultationLimbMaxSamples {
count = occultationLimbMaxSamples
}
first, last = math.NaN(), math.NaN()
previousTT, previousHidden := start, hiddenAtStart
for index := 0; index <= count; index++ {
tt := start + (end-start)*float64(index)/float64(count)
value, valueOK := gap(tt)
if !valueOK {
return 0, 0, false, false
}
hidden := value < 0
if index == 0 {
hidden = hiddenAtStart
} else if index == count {
hidden = hiddenAtEnd
}
if index > 0 && hidden != previousHidden {
root := occultationLimbBisect(previousTT, tt, previousHidden, gap)
if hidden && math.IsNaN(first) {
first = root
}
if !hidden {
last = root
}
}
previousTT, previousHidden = tt, hidden
}
return first, last, !math.IsNaN(first) || !math.IsNaN(last), true
}
// occultationLimbBisect 在相邻样本间求遮挡状态翻转的时刻;leftHidden 是左端的遮挡状态。
func occultationLimbBisect(left, right float64, leftHidden bool, gap func(float64) (float64, bool)) float64 {
for i := 0; i < 64 && right-left > occultationLimbRootDays; i++ {
mid := (left + right) / 2
value, ok := gap(mid)
if !ok {
break
}
if (value < 0) == leftHidden {
left = mid
} else {
right = mid
}
}
return (left + right) / 2
}
// planetOccultationInfoWithLimb 有月缘时按剖面判定行星月掩:只在圆月面间隙落进月缘起伏带的时段采样全方向月缘间隙,
// 取最外侧的外切与内切;月缘可新增或取消圆月面的掠掩与全掩,有正持续时间的接触一律按偏掩或全掩上报。剖面无值时退回圆月面结果。
func planetOccultationInfoWithLimb(greatestTT float64, config planetOccultationConfig, observer Observer, safetyMarginArcsec float64, location *time.Location, limb LunarLimb, inst *Instance) (PlanetOccultationInfo, bool) {
circularInfo, circularOK := planetOccultationInfoAtGreatest(greatestTT, config, observer, safetyMarginArcsec, location, inst)
if limb == nil {
return circularInfo, circularOK
}
info, state, ok := planetOccultationGreatestInfo(greatestTT, config, observer, safetyMarginArcsec, location, inst)
if !ok {
return circularInfo, circularOK
}
target := planetOccultationLimbTarget(config, observer)
lowKM, highKM, ok := occultationLimbRangeKM(greatestTT, limb, observer, target, inst)
if !ok {
return circularInfo, circularOK
}
ephemeris, ok := newPlanetLimbEphemeris(greatestTT, config, limb, inst)
if !ok {
return circularInfo, circularOK
}
// 月缘间隙 ≈ 圆月面间隙 −(月缘半径 − 参考半径),所以圆月面间隙不低于 bandHigh 必不接触、不高于 bandLow 必已接触。
// 起伏带以角秒计,外扩一点容差覆盖月心距在事件内的变化。
scale := state.moonSemidiameter / moonEquatorialRadiusKM
bandLow := (lowKM-moonEquatorialRadiusKM)*scale - occultationLimbBandPadArcsec
bandHigh := (highKM-moonEquatorialRadiusKM)*scale + occultationLimbBandPadArcsec
circular := func(internal bool) func(float64) (float64, bool) {
return func(tt float64) (float64, bool) { return ephemeris.circularGap(tt, observer, internal) }
}
limbGap := func(internal bool) func(float64) (float64, bool) {
return func(tt float64) (float64, bool) { return ephemeris.limbGap(tt, observer, internal) }
}
// solve 求一种接触的最外侧掩入、复现:圆月面间隙在 [bandLow, bandHigh] 之外的两端遮挡状态已知,只在带内采样。
solve := func(internal bool, circularMinimum float64) (float64, float64, bool, bool) {
if circularMinimum >= bandHigh {
return 0, 0, false, true
}
outerLeft, okLeft := occultationLimbCrossing(greatestTT, -1, bandHigh, circular(internal))
outerRight, okRight := occultationLimbCrossing(greatestTT, 1, bandHigh, circular(internal))
if !okLeft || !okRight {
return 0, 0, false, false
}
if circularMinimum > bandLow {
first, last, found, ok := occultationLimbOuterRoots(outerLeft, outerRight, false, false, limbGap(internal))
return first, last, found && !math.IsNaN(first) && !math.IsNaN(last), ok
}
innerLeft, okLeft := occultationLimbCrossing(greatestTT, -1, bandLow, circular(internal))
innerRight, okRight := occultationLimbCrossing(greatestTT, 1, bandLow, circular(internal))
if !okLeft || !okRight {
return 0, 0, false, false
}
first, _, foundLeft, okLeftRoots := occultationLimbOuterRoots(outerLeft, innerLeft, false, true, limbGap(internal))
_, last, foundRight, okRightRoots := occultationLimbOuterRoots(innerRight, outerRight, true, false, limbGap(internal))
return first, last, foundLeft && foundRight && !math.IsNaN(first) && !math.IsNaN(last), okLeftRoots && okRightRoots
}
externalImmersion, externalEmersion, external, ok := solve(false, state.externalContactMetric)
if !ok {
return circularInfo, circularOK
}
if !external {
return PlanetOccultationInfo{}, false
}
internalImmersion, internalEmersion, internal, ok := solve(true, state.internalContactMetric)
if !ok {
return circularInfo, circularOK
}
info.Type = OccultationPartial
info.ExternalImmersion = occultationTTToLocation(externalImmersion, location)
info.ExternalEmersion = occultationTTToLocation(externalEmersion, location)
if internal && internalImmersion > externalImmersion && internalEmersion < externalEmersion && internalImmersion < internalEmersion {
info.Type = OccultationTotal
info.HasInternalContacts = true
info.InternalImmersion = occultationTTToLocation(internalImmersion, location)
info.InternalEmersion = occultationTTToLocation(internalEmersion, location)
}
info.ContactsComplete = true
info.LunarLimbApplied = true
return info, true
}
// planetLimbEphemeris 掩甚前后 ±4 h 的地心月球、行星视位置与月球本体坐标架插值表;站心换算逐点精确计算。
type planetLimbEphemeris struct {
bodies []localEphemerisVectorNode
axes []localEphemerisVectorNode
base lunarLimbFrame
radiusKM float64
}
func newPlanetLimbEphemeris(greatestTT float64, config planetOccultationConfig, limb LunarLimb, inst *Instance) (*planetLimbEphemeris, bool) {
half := int(math.Round(starOccultationLimbNodeSpan / starOccultationLimbNodeDays))
ephemeris := &planetLimbEphemeris{
bodies: make([]localEphemerisVectorNode, 2*half+1),
axes: make([]localEphemerisVectorNode, 2*half+1),
radiusKM: config.equatorialRadiusKM,
}
for index := range ephemeris.bodies {
tt := greatestTT + float64(index-half)*starOccultationLimbNodeDays
moonRA, moonDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
moon := lunarEclipseLimbVector(moonRA, moonDec, HMoonAwayNWithInstance(tt, -1, inst))
planetRA, planetDec := config.apparentRaDecN(tt, -1)
planet := lunarEclipseLimbVector(planetRA, planetDec, config.earthDistanceN(tt, -1)*angularDiameterAstronomicalUnitKM)
frame := newLunarLimbFrame(limb, tt, moon, inst)
if !frame.valid || !finiteVector3(moon) || !finiteVector3(planet) {
return nil, false
}
if index == half {
ephemeris.base = frame
}
ephemeris.bodies[index] = localEphemerisVectorNode{tt: tt, first: moon, next: planet}
ephemeris.axes[index] = localEphemerisVectorNode{tt: tt, first: frame.axisX, next: frame.axisZ}
}
return ephemeris, true
}
// planetLimbTopocentric 是插值得到的站心月球、行星方向与距离(km),以及该时刻的月球本体坐标架。
type planetLimbTopocentric struct {
moon, planet [3]float64
moonDistance, planetRadius float64
frame lunarLimbFrame
}
func (ephemeris *planetLimbEphemeris) topocentricAt(tt float64, observer Observer) (planetLimbTopocentric, bool) {
moon, planet, ok := interpolateLocalEphemerisVectors(ephemeris.bodies, tt)
if !ok {
return planetLimbTopocentric{}, false
}
axisX, axisZ, ok := interpolateLocalEphemerisVectors(ephemeris.axes, tt)
if !ok {
return planetLimbTopocentric{}, false
}
ut := TT2UTC(tt)
toTopocentric := func(vector [3]float64) (float64, float64, float64, bool) {
distance := math.Sqrt(lunarLimbDot(vector, vector))
ra := normalizeRA(math.Atan2(vector[1], vector[0]) / rad)
dec := math.Asin(clampUnit(vector[2]/distance)) / rad
topoRA, topoDec := TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, ut, distance/angularDiameterAstronomicalUnitKM, observer.Height)
topoDistance := topocentricDistanceKM(ra, dec, distance, observer, ut)
return normalizeRA(topoRA), topoDec, topoDistance, finite(topoRA) && finite(topoDec) && finite(topoDistance) && topoDistance > 0
}
moonRA, moonDec, moonDistance, okMoon := toTopocentric(moon)
planetRA, planetDec, planetDistance, okPlanet := toTopocentric(planet)
if !okMoon || !okPlanet || planetDistance <= ephemeris.radiusKM {
return planetLimbTopocentric{}, false
}
frame := ephemeris.base
if frame.axisZ, ok = lunarLimbNormalize(axisZ); !ok {
return planetLimbTopocentric{}, false
}
if frame.axisX, ok = lunarLimbNormalize(lunarLimbReject(axisX, frame.axisZ)); !ok {
return planetLimbTopocentric{}, false
}
frame.axisY = lunarLimbCross(frame.axisZ, frame.axisX)
return planetLimbTopocentric{
moon: lunarEclipseLimbVector(moonRA, moonDec, moonDistance),
planet: lunarEclipseLimbVector(planetRA, planetDec, 1),
moonDistance: moonDistance,
planetRadius: math.Asin(ephemeris.radiusKM / planetDistance),
frame: frame,
}, true
}
// circularGap 圆月面外切或内切间隙(角秒),与 planetOccultationStateAt 同口径。
func (ephemeris *planetLimbEphemeris) circularGap(tt float64, observer Observer, internal bool) (float64, bool) {
state, ok := ephemeris.topocentricAt(tt, observer)
if !ok {
return 0, false
}
separation := math.Acos(clampUnit(lunarLimbDot(state.moon, state.planet) / state.moonDistance))
moonRadius := math.Asin(moonEquatorialRadiusKM / state.moonDistance)
if internal {
return (separation - moonRadius + state.planetRadius) * angularDiameterArcsecPerRadian, true
}
return (separation - moonRadius - state.planetRadius) * angularDiameterArcsecPerRadian, true
}
// limbGap 全方向月缘外切或内切间隙(角秒)。
func (ephemeris *planetLimbEphemeris) limbGap(tt float64, observer Observer, internal bool) (float64, bool) {
state, ok := ephemeris.topocentricAt(tt, observer)
if !ok {
return 0, false
}
view, ok := state.frame.viewToward(state.moon, state.planet)
if !ok {
return 0, false
}
contact := lunarLimbContact{
frame: &state.frame,
view: view,
distanceKM: state.moonDistance,
separation: math.Acos(clampUnit(lunarLimbDot(state.moon, state.planet) / state.moonDistance)),
targetRadius: state.planetRadius,
}
if internal {
inner, ok := contact.innerRadius(true)
if !ok {
return 0, false
}
return (contact.separation - math.Abs(inner-contact.targetRadius)) * angularDiameterArcsecPerRadian, true
}
outer, ok := contact.outerRadius()
if !ok {
return 0, false
}
return (contact.separation - outer - contact.targetRadius) * angularDiameterArcsecPerRadian, true
}
+86
View File
@@ -0,0 +1,86 @@
package basic
// 剖面不报告半径范围时,月掩须在当时视向上扫满一圈求范围,结果与报告范围的同一剖面一致。
import (
"math"
"testing"
"time"
"b612.me/astro/moon/limb"
)
// unrangedLunarLimb 只暴露 LimbRadiusKM,迫使月掩走整圈扫描。
type unrangedLunarLimb struct{ profile *limb.Profile }
func (u unrangedLunarLimb) LimbRadiusKM(lon, lat, angle float64) (float64, bool) {
return u.profile.LimbRadiusKM(lon, lat, angle)
}
// limbBruteRangeKM 以剖面步长的 1/8 细扫一圈,作整圈扫描的对拍基准。
func limbBruteRangeKM(t *testing.T, tt float64, profile LunarLimb, observer Observer, ra, dec float64) (float64, float64) {
t.Helper()
geoRA, geoDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, nil)
frame := newLunarLimbFrame(profile, tt, lunarEclipseLimbVector(geoRA, geoDec, HMoonAwayNWithInstance(tt, -1, nil)), nil)
topoRA, topoDec := moonTopocentricApparentRaDec(tt, observer, -1, nil)
view, ok := frame.viewToward(lunarEclipseLimbVector(topoRA, topoDec, 1), lunarEclipseLimbVector(ra, dec, 1))
if !frame.valid || !ok {
t.Fatal("limb view failed")
}
low, high := math.Inf(1), math.Inf(-1)
for offset := 0.0; offset < 360; offset += frame.stepDeg / 8 {
radius, ok := frame.radiusAt(view, offset)
if !ok {
t.Fatal("limb radius failed")
}
low, high = math.Min(low, radius), math.Max(high, radius)
}
return low, high
}
func TestOccultationLimbRangeScansFullCircleWithoutProvider(t *testing.T) {
profile := limb.Default()
unranged := unrangedLunarLimb{profile: profile}
observer := Observer{Longitude: 41.5907, Latitude: -55.07036}
tt := occultationTimeToTT(time.Date(2026, 2, 11, 3, 47, 15, 0, time.UTC))
ra, dec := starApparentRaDec(tt, limbTestAntares, observer, nil)
wantLow, wantHigh := limbBruteRangeKM(t, tt, unranged, observer, ra, dec)
// 剖面网格节点即极值,整圈扫描与细扫只差插值误差;扫描步长放粗会漏掉极值。
const tolerance = 0.05
starLow, starHigh, ok := starOccultationLimbRange(tt, limbTestAntares, observer, unranged, nil)
if !ok {
t.Fatal("star range failed")
}
target := func(float64) (float64, float64, float64, float64, bool) { return ra, dec, 0, 0, true }
low, high, ok := occultationLimbRangeKM(tt, unranged, observer, target, nil)
if !ok {
t.Fatal("occultation range failed")
}
for _, got := range []struct {
name string
low, high float64
pad float64
}{{"star", starLow, starHigh, starOccultationLimbUnrangedPadKM}, {"planet", low, high, occultationLimbUnrangedPadKM}} {
if math.Abs(got.low+got.pad-wantLow) > tolerance || math.Abs(got.high-got.pad-wantHigh) > tolerance {
t.Errorf("%s: range %.3f–%.3f km, brute force %.3f–%.3f km", got.name, got.low+got.pad, got.high-got.pad, wantLow, wantHigh)
}
}
rangedLow, rangedHigh := profile.RadiusRangeKM()
if wantLow < rangedLow || wantHigh > rangedHigh || wantHigh-wantLow < 1 {
t.Errorf("brute force %.3f–%.3f km outside table range %.3f–%.3f km or too flat", wantLow, wantHigh, rangedLow, rangedHigh)
}
}
func TestStarOccultationUnrangedLimbMatchesRanged(t *testing.T) {
const lon, lat = 41.5907, -55.07036 + 0.4/111.2
ranged := limbTestAntaresEvent(t, lon, lat, limb.Default())
unranged := limbTestAntaresEvent(t, lon, lat, unrangedLunarLimb{profile: limb.Default()})
if len(ranged) != 1 || len(unranged) != 1 || ranged[0].Type != OccultationGrazing || unranged[0].Type != ranged[0].Type {
t.Fatalf("ranged %+v vs unranged %+v, want the same limb graze", ranged, unranged)
}
for _, pair := range [][2]time.Time{{ranged[0].Immersion, unranged[0].Immersion}, {ranged[0].Emersion, unranged[0].Emersion}} {
if d := math.Abs(pair[0].Sub(pair[1]).Seconds()); d > 0.05 {
t.Errorf("unranged limb moved a contact by %.3f s", d)
}
}
}
+11 -4
View File
@@ -4,6 +4,8 @@ import (
"math"
"testing"
"time"
"b612.me/astro/internal/ephemtest"
)
// 掩带两条横向口径的契约:WidthKM 是中心线采样处的地面横向宽度,南北限是外接触锥的擦边切点轨迹,
@@ -240,6 +242,7 @@ func TestPlanetOccultationWidthKMIsGroundCrossTrackWidth(t *testing.T) {
}
func TestPlanetOccultationLimitSeparationMatchesExportedLimits(t *testing.T) {
values := ephemtest.NewValues(t, "occultation_limit_anchors")
for _, fixture := range occultationLimitFixtures() {
path := occultationLimitPath(t, fixture)
nearestIndex, nearestDelta := 0, math.Inf(1)
@@ -279,15 +282,17 @@ func TestPlanetOccultationLimitSeparationMatchesExportedLimits(t *testing.T) {
{name: "greatest limit separation", got: path.GreatestLimitSeparationKM, want: fixture.limitSeparationKM},
{name: "greatest total width", got: path.GreatestTotalWidthKM, want: fixture.totalWidthKM},
} {
if difference := math.Abs(anchor.got - anchor.want); difference > occultationLimitAnchorToleranceKM {
want := values.Want(fixture.name+"/"+anchor.name, anchor.want, anchor.got)
if difference := math.Abs(anchor.got - want); difference > occultationLimitAnchorToleranceKM {
t.Fatalf("%s: %s=%.6f km, want %.6f km within %.3f km",
fixture.name, anchor.name, anchor.got, anchor.want, occultationLimitAnchorToleranceKM)
fixture.name, anchor.name, anchor.got, want, occultationLimitAnchorToleranceKM)
}
}
ratio := path.GreatestLimitSeparationKM / path.Greatest.WidthKM
if math.Abs(ratio-fixture.limitRatio) > occultationLimitRatioTolerance {
wantRatio := values.Want(fixture.name+"/limit ratio", fixture.limitRatio, ratio)
if math.Abs(ratio-wantRatio) > occultationLimitRatioTolerance {
t.Fatalf("%s: limit separation/width=%.6f, want %.6f within %.4f",
fixture.name, ratio, fixture.limitRatio, occultationLimitRatioTolerance)
fixture.name, ratio, wantRatio, occultationLimitRatioTolerance)
}
if path.GreatestTotalWidthKM >= path.Greatest.WidthKM {
t.Fatalf("%s: total width=%.6f must stay below the outer width=%.6f",
@@ -342,6 +347,8 @@ func TestStarOccultationLimitSeparationMatchesExportedLimits(t *testing.T) {
// 于是 Saturn 2024-08-21 的 CenterLine[160] 少 47.9 km(3810.7491 对 3858.7641)、
// Venus 1227-05-19 的 CenterLine[15] 少 158.6 km(6047.4858 对 6206.0535)。
func TestPlanetOccultationGrazingEndpointWidthAnchors(t *testing.T) {
// 夹具按旧模型几何选取,用来覆盖该边界分支。
ephemtest.PinLegacy(t)
testCases := []struct {
name string
planet OccultationPlanet
+62 -60
View File
@@ -5,6 +5,8 @@ import (
"sort"
"sync"
"time"
"b612.me/astro/internal/geodata"
)
const (
@@ -29,16 +31,12 @@ const (
occultationPathBoundaryBranchJumpKM = 120.0
occultationPathBoundaryBranchSpeedKMPerSecond = 10.0
occultationPathEarthEquatorialRadiusKM = 6378.1366
occultationPathEarthPolarRatio = 0.99664719
occultationPathAstronomicalUnitKM = 149597870.7
occultationPathEarthPolarRatio = 0.99664719
occultationPathAstronomicalUnitKM = 149597870.7
)
// FindStarOccultationPaths 搜索单颗点源恒星月掩的全球掩带。
// 查询窗口按全球几何掩甚点选择事件,端点容差为 10 ms,与数值根精度一致。返回路径扩展到完整全球起止点;函数不会加载内嵌星表,调用者需显式提供坐标。
// FindStarOccultationPaths searches the global lunar-occultation footprint of one point-source star.
// The query window selects events by global geometric greatest, with a 10 ms endpoint tolerance matching the numerical root precision. Each returned path expands to its complete global start and end; the function does not load the embedded catalog.
func FindStarOccultationPaths(start, end time.Time, star StarCoordinate, options OccultationPathOptions) ([]StarOccultationPath, error) {
// FindStarOccultationPathsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func FindStarOccultationPathsWithInstance(start, end time.Time, star StarCoordinate, options OccultationPathOptions, inst *Instance) ([]StarOccultationPath, error) {
if err := validateOccultationTimeRange(start, end); err != nil {
return nil, err
}
@@ -60,11 +58,11 @@ func FindStarOccultationPaths(start, end time.Time, star StarCoordinate, options
candidateEndTT,
starOccultationCoarseStepDays(coarseOptions),
star,
0,
0, inst,
)
paths := make([]StarOccultationPath, 0, len(candidates))
for _, seedTT := range candidates {
path, ok, err := starOccultationPathAtSeed(seedTT, star, options, start, end)
path, ok, err := starOccultationPathAtSeed(seedTT, star, options, start, end, inst)
if err != nil {
return nil, err
}
@@ -122,10 +120,11 @@ func starOccultationPathAtSeed(
seedTT float64,
star StarCoordinate,
options OccultationPathOptions,
selectionStart, selectionEnd time.Time,
) (StarOccultationPath, bool, error) {
selectionStart, selectionEnd time.Time, instances ...*Instance) (StarOccultationPath, bool, error) {
inst := instanceValue(instances)
location := selectionStart.Location()
cache := newStarOccultationEventCache(star)
cache := newStarOccultationEventCache(star, inst)
cache.prepareLocalEphemeris(seedTT)
frameAt := cache.frameAt
candidateFrameAt := cache.candidateFrameAt
@@ -170,14 +169,14 @@ func starOccultationPathAtSeed(
return StarOccultationPath{}, false, ErrOccultationPathSamplingLimit
}
start := starOccultationPathBoundaryEndpointWithFrame(outerStart, frameAt, location, 1)
end := starOccultationPathBoundaryEndpointWithFrame(outerEnd, frameAt, location, -1)
start := starOccultationPathBoundaryEndpointWithFrame(outerStart, frameAt, location, 1, inst)
end := starOccultationPathBoundaryEndpointWithFrame(outerEnd, frameAt, location, -1, inst)
if !start.valid || !end.valid {
return StarOccultationPath{}, false, nil
}
exactFrameAt := frameAt
if options.Algorithm != OccultationPathAlgorithmExact {
optimized := newStarOccultationEventCache(star)
optimized := newStarOccultationEventCache(star, inst)
optimized.preparePathEphemeris(seedTT, options.Algorithm)
if optimized.local.dense {
cache = optimized
@@ -220,6 +219,10 @@ func starOccultationPathAtSeed(
path.SouthernLimit = occultationStationCorrectLimitSeries(
path.SouthernLimit, frameAt, cache.riseSetContextAt, false, location,
)
var northApplied, southApplied bool
path.NorthernLimit, northApplied = occultationLimbCorrectLimitSeries(path.NorthernLimit, cache.riseSetContextAt, options.LunarLimb, location, inst)
path.SouthernLimit, southApplied = occultationLimbCorrectLimitSeries(path.SouthernLimit, cache.riseSetContextAt, options.LunarLimb, location, inst)
path.LunarLimbApplied = northApplied || southApplied
path.GreatestLimitSeparationKM, _ = occultationPathLimitSeparations(
path.NorthernLimit, path.SouthernLimit, greatestTT,
)
@@ -458,9 +461,11 @@ func occultationPathContourStepDays(options OccultationPathOptions) float64 {
return stepDays
}
func starOccultationPathWindow(seedTT, startTT, endTT float64, star StarCoordinate, center bool) (float64, float64, bool) {
func starOccultationPathWindow(seedTT, startTT, endTT float64, star StarCoordinate, center bool, instances ...*Instance) (float64, float64, bool) {
inst := instanceValue(instances)
return starOccultationPathWindowWithFrame(seedTT, startTT, endTT, func(tt float64) (occultationPathFrame, bool) {
return starOccultationPathFrameAt(tt, star)
return starOccultationPathFrameAt(tt, star, inst)
}, center)
}
@@ -511,9 +516,11 @@ func occultationPathWindowWithCandidateFrames(
)
}
func starOccultationPathGreatest(seedTT, startTT, endTT float64, star StarCoordinate) float64 {
func starOccultationPathGreatest(seedTT, startTT, endTT float64, star StarCoordinate, instances ...*Instance) float64 {
inst := instanceValue(instances)
return starOccultationPathGreatestWithFrame(seedTT, startTT, endTT, func(tt float64) (occultationPathFrame, bool) {
return starOccultationPathFrameAt(tt, star)
return starOccultationPathFrameAt(tt, star, inst)
})
}
@@ -746,6 +753,8 @@ type starOccultationEphemerisState struct {
}
type starOccultationEventCache struct {
inst *Instance
star StarCoordinate
states map[uint64]starOccultationEphemerisState
frames map[uint64]planetOccultationFrameCacheEntry
@@ -753,8 +762,10 @@ type starOccultationEventCache struct {
local *starOccultationLocalEphemeris
}
func newStarOccultationEventCache(star StarCoordinate) *starOccultationEventCache {
cache := &starOccultationEventCache{
func newStarOccultationEventCache(star StarCoordinate, instances ...*Instance) *starOccultationEventCache {
inst := instanceValue(instances)
cache := &starOccultationEventCache{inst: inst,
star: star,
states: make(map[uint64]starOccultationEphemerisState),
frames: make(map[uint64]planetOccultationFrameCacheEntry),
@@ -785,7 +796,7 @@ func (cache *starOccultationEventCache) stateAt(tt float64) starOccultationEphem
state, interpolated = cache.local.stateAt(tt)
}
if !interpolated {
state = starOccultationEphemerisStateAt(tt, cache.star)
state = starOccultationEphemerisStateAt(tt, cache.star, cache.inst)
}
cache.states[key] = state
return state
@@ -793,7 +804,7 @@ func (cache *starOccultationEventCache) stateAt(tt float64) starOccultationEphem
func (cache *starOccultationEventCache) prepareLocalEphemeris(center float64) {
if cache.local == nil {
cache.local = newStarOccultationLocalEphemeris(center, cache.star)
cache.local = newStarOccultationLocalEphemeris(center, cache.star, cache.inst)
}
}
@@ -851,15 +862,19 @@ type occultationPathEndpoint struct {
func (f occultationPathFrame) moonProjectionX() float64 { return occultationPathDot(f.moon, f.first) }
func (f occultationPathFrame) moonProjectionY() float64 { return occultationPathDot(f.moon, f.second) }
func starOccultationPathFrameAt(tt float64, star StarCoordinate) (occultationPathFrame, bool) {
return starOccultationPathFrameFromState(starOccultationEphemerisStateAt(tt, star))
func starOccultationPathFrameAt(tt float64, star StarCoordinate, instances ...*Instance) (occultationPathFrame, bool) {
inst := instanceValue(instances)
return starOccultationPathFrameFromState(starOccultationEphemerisStateAt(tt, star, inst))
}
func starOccultationEphemerisStateAt(tt float64, star StarCoordinate) starOccultationEphemerisState {
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1)
moonDistanceKM := HMoonAwayN(tt, -1)
func starOccultationEphemerisStateAt(tt float64, star StarCoordinate, instances ...*Instance) starOccultationEphemerisState {
inst := instanceValue(instances)
moonRA, moonDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
moonDistanceKM := HMoonAwayNWithInstance(tt, -1, inst)
// 方向与距离必须来自同一次三维推进,否则视差修正会退回历元距离。
starRA, starDec, starDistanceAU := starApparentRaDecDistanceGeocentric(tt, star)
starRA, starDec, starDistanceAU := starApparentRaDecDistanceGeocentric(tt, star, inst)
starDistanceKM := starDistanceAU * occultationPathAstronomicalUnitKM
return starOccultationEphemerisState{
moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistanceKM,
@@ -906,8 +921,9 @@ func starOccultationPathBoundaryEndpointWithFrame(
tt float64,
frameAt occultationPathFrameFunc,
location *time.Location,
direction int,
) occultationPathEndpoint {
direction int, instances ...*Instance) occultationPathEndpoint {
inst := instanceValue(instances)
if _, ok := frameAt(tt); !ok {
return occultationPathEndpoint{}
}
@@ -919,15 +935,17 @@ func starOccultationPathBoundaryEndpointWithFrame(
}
vector, _, valid := occultationPathBoundaryTangent(candidateFrame)
if valid {
return occultationPathEndpoint{point: occultationPathPointFromVector(candidateTT, vector, 0, location), valid: true}
return occultationPathEndpoint{point: occultationPathPointFromVector(candidateTT, vector, 0, location, inst), valid: true}
}
}
return occultationPathEndpoint{}
}
func starOccultationPathCenterPoint(tt float64, star StarCoordinate, location *time.Location) (OccultationPathPoint, bool) {
func starOccultationPathCenterPoint(tt float64, star StarCoordinate, location *time.Location, instances ...*Instance) (OccultationPathPoint, bool) {
inst := instanceValue(instances)
return starOccultationPathCenterPointWithFrame(tt, func(candidateTT float64) (occultationPathFrame, bool) {
return starOccultationPathFrameAt(candidateTT, star)
return starOccultationPathFrameAt(candidateTT, star, inst)
}, location)
}
@@ -1227,7 +1245,7 @@ func occultationPathBoundaryLine(frame occultationPathFrame, theta float64) (dis
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
a := direction.x*direction.x + direction.y*direction.y + direction.z*direction.z/polarRatioSquared
b = origin.x*direction.x + origin.y*direction.y + origin.z*direction.z/polarRatioSquared
c := origin.x*origin.x + origin.y*origin.y + origin.z*origin.z/polarRatioSquared - occultationPathEarthEquatorialRadiusKM*occultationPathEarthEquatorialRadiusKM
c := origin.x*origin.x + origin.y*origin.y + origin.z*origin.z/polarRatioSquared - geodata.EarthEquatorialRadiusKM*geodata.EarthEquatorialRadiusKM
discriminant = b*b - a*c
scale = math.Max(math.Abs(b*b), math.Abs(a*c))
return discriminant, b, scale, a > 0 && finite(discriminant)
@@ -1241,9 +1259,11 @@ func occultationPathBoundaryIntersection(frame occultationPathFrame, theta, tole
return occultationEarthLineIntersectionWithTolerance(origin, direction, tolerance)
}
func occultationPathPointFromVector(tt float64, vector occultationPathVector, width float64, location *time.Location) OccultationPathPoint {
func occultationPathPointFromVector(tt float64, vector occultationPathVector, width float64, location *time.Location, instances ...*Instance) OccultationPathPoint {
inst := instanceValue(instances)
lon, lat := occultationPathGeodetic(tt, vector)
moonRA, moonDec := moonTopocentricApparentRaDec(tt, Observer{Longitude: lon, Latitude: lat}, -1)
moonRA, moonDec := moonTopocentricApparentRaDec(tt, Observer{Longitude: lon, Latitude: lat}, -1, inst)
return OccultationPathPoint{
Time: occultationTTToLocation(tt, location),
Longitude: lon,
@@ -1419,17 +1439,7 @@ func occultationPathDistanceKM(a, b OccultationPathPoint) float64 {
}
func occultationPathDistanceKMValues(lon1, lat1, lon2, lat2 float64) float64 {
lat1 *= math.Pi / 180
lat2 *= math.Pi / 180
dLat := lat2 - lat1
dLon := (lon2 - lon1) * math.Pi / 180
dLon = math.Mod(dLon+math.Pi, 2*math.Pi)
if dLon < 0 {
dLon += 2 * math.Pi
}
dLon -= math.Pi
h := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(lat1)*math.Cos(lat2)*math.Sin(dLon/2)*math.Sin(dLon/2)
return 2 * occultationPathEarthEquatorialRadiusKM * math.Asin(math.Sqrt(math.Min(1, h)))
return geodata.EarthEquatorialRadiusKM * geodata.CentralAngle(lon1, lat1, lon2, lat2)
}
type occultationPathVector struct{ x, y, z float64 }
@@ -1504,14 +1514,14 @@ func occultationPathTrackReference(frame occultationPathFrame) (occultationPathV
if metricRadius <= 1e-9 || !finite(metricRadius) {
return occultationPathVector{}, false
}
return occultationPathScale(closest, occultationPathEarthEquatorialRadiusKM/metricRadius), true
return occultationPathScale(closest, geodata.EarthEquatorialRadiusKM/metricRadius), true
}
func occultationEarthLineIntersectionWithTolerance(origin, direction occultationPathVector, tolerance float64) (occultationPathVector, float64, bool) {
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
a := direction.x*direction.x + direction.y*direction.y + direction.z*direction.z/polarRatioSquared
b := origin.x*direction.x + origin.y*direction.y + origin.z*direction.z/polarRatioSquared
c := origin.x*origin.x + origin.y*origin.y + origin.z*origin.z/polarRatioSquared - occultationPathEarthEquatorialRadiusKM*occultationPathEarthEquatorialRadiusKM
c := origin.x*origin.x + origin.y*origin.y + origin.z*origin.z/polarRatioSquared - geodata.EarthEquatorialRadiusKM*geodata.EarthEquatorialRadiusKM
discriminant := b*b - a*c
if discriminant < -tolerance || a <= 0 {
return occultationPathVector{}, 0, false
@@ -1542,7 +1552,7 @@ func occultationPathGeodeticWithSidereal(
vector occultationPathVector,
siderealDegrees float64,
) (float64, float64) {
longitude := normalizeLongitude(math.Atan2(vector.y, vector.x)*180/math.Pi - siderealDegrees)
longitude := geodata.NormalizeLongitude(math.Atan2(vector.y, vector.x)*180/math.Pi - siderealDegrees)
return longitude, occultationPathGeodeticLatitude(vector)
}
@@ -1577,11 +1587,3 @@ func occultationPathEarthFixedVectorWithRotation(
z: vector.z,
}
}
func normalizeLongitude(longitude float64) float64 {
longitude = math.Mod(longitude+180, 360)
if longitude < 0 {
longitude += 360
}
return longitude - 180
}
+7 -5
View File
@@ -1,11 +1,13 @@
package basic
import "math"
import (
"math"
)
const (
occultationDenseEphemerisNodeCount = 193
occultationDenseEphemerisStepDays = 30.0 / 1440.0
occultationDenseEphemerisTolerance = 5e-11
occultationDenseEphemerisTolerance = 1e-8 // relaxed for vector-based apparent positions
)
// Width extrema near a limb can change branches under tiny frame differences.
@@ -23,7 +25,7 @@ func correctOccultationCenterWidths(points []OccultationPathPoint, frameAt occul
func (cache *starOccultationEventCache) preparePathEphemeris(center float64, algorithm OccultationPathAlgorithm) {
if algorithm != OccultationPathAlgorithmExact {
nodes := newDenseOccultationEphemerisNodes(center, func(tt float64) ([3]float64, [3]float64) {
state := starOccultationEphemerisStateAt(tt, cache.star)
state := starOccultationEphemerisStateAt(tt, cache.star, cache.inst)
moon := occultationPathRaDecVector(state.moonRA, state.moonDec, state.moonDistanceKM)
distance := state.starDistanceKM
if distance <= 0 {
@@ -33,7 +35,7 @@ func (cache *starOccultationEventCache) preparePathEphemeris(center float64, alg
return [3]float64{moon.x, moon.y, moon.z}, [3]float64{target.x, target.y, target.z}
})
if len(nodes) > 0 {
cache.local = newStarOccultationLocalEphemerisFromNodes(center, cache.star, nodes, true)
cache.local = newStarOccultationLocalEphemerisFromNodes(center, cache.star, nodes, true, cache.inst)
}
}
cache.prepareLocalEphemeris(center)
@@ -42,7 +44,7 @@ func (cache *starOccultationEventCache) preparePathEphemeris(center float64, alg
func (cache *planetOccultationEventCache) preparePathEphemeris(center float64, algorithm OccultationPathAlgorithm) {
if algorithm != OccultationPathAlgorithmExact {
nodes := newDenseOccultationEphemerisNodes(center, func(tt float64) ([3]float64, [3]float64) {
state := planetOccultationEphemerisStateAt(tt, cache.config)
state := planetOccultationEphemerisStateAt(tt, cache.config, cache.inst)
moon := occultationPathRaDecVector(state.moonRA, state.moonDec, state.moonDistanceKM)
target := occultationPathRaDecVector(state.planetRA, state.planetDec, state.planetDistanceKM)
return [3]float64{moon.x, moon.y, moon.z}, [3]float64{target.x, target.y, target.z}
+2
View File
@@ -6,6 +6,8 @@ import (
"reflect"
"testing"
"time"
_ "b612.me/astro/planet" // ensure semianalytic engine registration
)
func TestOccultationPathAlgorithmOptions(t *testing.T) {
+109 -86
View File
@@ -52,12 +52,9 @@ func (state planetOccultationState) movingDiskContactState() movingDiskContactSt
}
}
// FindPlanetOccultations 搜索固定观测点的有限盘面行星月掩。
// 经度东为正、纬度北为正,单位为度;高度为平均海平面以上米数。目标位置、视差和视半径会在每次候选、掩甚和接触计算时重新计算。
// FindPlanetOccultations searches one finite-disk planet at a fixed observing site.
// Longitude is east-positive in degrees, latitude is north-positive in degrees, and height is the observer elevation above mean sea level in meters. The target position, parallax, and semidiameter are recomputed at every candidate, greatest, and contact evaluation.
func FindPlanetOccultations(start, end time.Time, planet OccultationPlanet, longitude, latitude, height float64,
options OccultationSearchOptions) ([]PlanetOccultationInfo, error) {
// FindPlanetOccultationsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func FindPlanetOccultationsWithInstance(start, end time.Time, planet OccultationPlanet, longitude, latitude, height float64,
options OccultationSearchOptions, inst *Instance) ([]PlanetOccultationInfo, error) {
if err := validateOccultationTimeRange(start, end); err != nil {
return nil, err
}
@@ -72,16 +69,16 @@ func FindPlanetOccultations(start, end time.Time, planet OccultationPlanet, long
return nil, err
}
config, _ := planetOccultationConfigFor(planet)
config, _ := planetOccultationConfigFor(planet, inst)
startTT := occultationTimeToTT(start)
endTT := occultationTimeToTT(end)
resultLocation := start.Location()
candidates := planetOccultationCandidateGreatestTimes(
startTT, endTT, planetOccultationCoarseStepDays(options), config, &observer, options.SafetyMarginArcsec,
startTT, endTT, planetOccultationCoarseStepDays(options), config, &observer, options.SafetyMarginArcsec, inst,
)
results := make([]PlanetOccultationInfo, 0, len(candidates))
for _, greatestTT := range candidates {
info, ok := planetOccultationInfoAtGreatest(greatestTT, config, observer, options.SafetyMarginArcsec, resultLocation)
info, ok := planetOccultationInfoWithLimb(greatestTT, config, observer, options.SafetyMarginArcsec, resultLocation, options.LunarLimb, inst)
if !ok {
continue
}
@@ -96,12 +93,9 @@ func FindPlanetOccultations(start, end time.Time, planet OccultationPlanet, long
return results, nil
}
// FindBestPlanetOccultations 返回窗口内每次有限盘面行星月掩的全球海平面几何掩甚点。
// 地心数据只用于搜索初值;返回位置与 FindPlanetOccultationPaths 一致,地平线可见性只报告、不参与点选择。距离查询端点 10 ms 内的掩甚时刻也会包含,与数值根精度一致。
// FindBestPlanetOccultations returns the global geometric greatest point at sea level for every finite-disk planetary occultation in the window.
// Geocentric data only seeds the search. The returned location matches FindPlanetOccultationPaths; horizon visibility is reported but does not select the point. A greatest instant within 10 ms of either query endpoint is included, matching the numerical root precision.
func FindBestPlanetOccultations(start, end time.Time, planet OccultationPlanet,
options OccultationSearchOptions) ([]PlanetOccultationInfo, error) {
// FindBestPlanetOccultationsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func FindBestPlanetOccultationsWithInstance(start, end time.Time, planet OccultationPlanet,
options OccultationSearchOptions, inst *Instance) ([]PlanetOccultationInfo, error) {
if err := validateOccultationTimeRange(start, end); err != nil {
return nil, err
}
@@ -112,7 +106,7 @@ func FindBestPlanetOccultations(start, end time.Time, planet OccultationPlanet,
return nil, err
}
config, _ := planetOccultationConfigFor(planet)
config, _ := planetOccultationConfigFor(planet, inst)
startTT := occultationTimeToTT(start)
endTT := occultationTimeToTT(end)
selectionStartTT := startTT - occultationEventSelectionToleranceDays
@@ -121,15 +115,15 @@ func FindBestPlanetOccultations(start, end time.Time, planet OccultationPlanet,
candidateEndTT := endTT + occultationPathSearchSpanDays
resultLocation := start.Location()
candidates := planetOccultationCandidateGreatestTimes(
candidateStartTT, candidateEndTT, planetOccultationCoarseStepDays(options), config, nil, options.SafetyMarginArcsec,
candidateStartTT, candidateEndTT, planetOccultationCoarseStepDays(options), config, nil, options.SafetyMarginArcsec, inst,
)
results := make([]PlanetOccultationInfo, 0, len(candidates))
for _, seedTT := range candidates {
greatestTT, observer, _, observerOK := planetOccultationBestObserver(seedTT, selectionStartTT, selectionEndTT, config)
greatestTT, observer, _, observerOK := planetOccultationBestObserver(seedTT, selectionStartTT, selectionEndTT, config, inst)
if !observerOK {
continue
}
info, ok := planetOccultationInfoAtGreatest(greatestTT, config, observer, options.SafetyMarginArcsec, resultLocation)
info, ok := planetOccultationInfoWithLimb(greatestTT, config, observer, options.SafetyMarginArcsec, resultLocation, options.LunarLimb, inst)
if !ok {
continue
}
@@ -144,51 +138,53 @@ func FindBestPlanetOccultations(start, end time.Time, planet OccultationPlanet,
return results, nil
}
func planetOccultationConfigFor(planet OccultationPlanet) (planetOccultationConfig, bool) {
func planetOccultationConfigFor(planet OccultationPlanet, instances ...*Instance) (planetOccultationConfig, bool) {
inst := instanceValue(instances)
config := planetOccultationConfig{planet: planet}
switch planet {
case OccultationMercury:
config.planetIndex = 1
config.equatorialRadiusKM = mercuryEquatorialRadiusKM
config.apparentRaDecN = MercuryApparentRaDecN
config.earthDistanceN = EarthMercuryAwayN
config.semidiameterN = MercurySemidiameterN
config.apparentRaDecN = func(jde float64, n int) (float64, float64) { return MercuryApparentRaDecNWithInstance(jde, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthMercuryAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return MercurySemidiameterNWithInstance(jde, n, inst) }
case OccultationVenus:
config.planetIndex = 2
config.equatorialRadiusKM = venusEquatorialRadiusKM
config.apparentRaDecN = VenusApparentRaDecN
config.earthDistanceN = EarthVenusAwayN
config.semidiameterN = VenusSemidiameterN
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return VenusApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthVenusAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return VenusSemidiameterNWithInstance(jde, n, inst) }
case OccultationMars:
config.planetIndex = 3
config.equatorialRadiusKM = marsEquatorialRadiusKM
config.apparentRaDecN = MarsApparentRaDecN
config.earthDistanceN = EarthMarsAwayN
config.semidiameterN = MarsSemidiameterN
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return MarsApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthMarsAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return MarsSemidiameterNWithInstance(jde, n, inst) }
case OccultationJupiter:
config.planetIndex = 4
config.equatorialRadiusKM = jupiterEquatorialRadiusKM
config.apparentRaDecN = JupiterApparentRaDecN
config.earthDistanceN = EarthJupiterAwayN
config.semidiameterN = JupiterSemidiameterN
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return JupiterApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthJupiterAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return JupiterSemidiameterNWithInstance(jde, n, inst) }
case OccultationSaturn:
config.planetIndex = 5
config.equatorialRadiusKM = saturnEquatorialRadiusKM
config.apparentRaDecN = SaturnApparentRaDecN
config.earthDistanceN = EarthSaturnAwayN
config.semidiameterN = SaturnSemidiameterN
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return SaturnApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthSaturnAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return SaturnSemidiameterNWithInstance(jde, n, inst) }
case OccultationUranus:
config.planetIndex = 6
config.equatorialRadiusKM = uranusEquatorialRadiusKM
config.apparentRaDecN = UranusApparentRaDecN
config.earthDistanceN = EarthUranusAwayN
config.semidiameterN = UranusSemidiameterN
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return UranusApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthUranusAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return UranusSemidiameterNWithInstance(jde, n, inst) }
case OccultationNeptune:
config.planetIndex = 7
config.equatorialRadiusKM = neptuneEquatorialRadiusKM
config.apparentRaDecN = NeptuneApparentRaDecN
config.earthDistanceN = EarthNeptuneAwayN
config.semidiameterN = NeptuneSemidiameterN
config.apparentRaDecN = func(jd float64, n int) (float64, float64) { return NeptuneApparentRaDecNWithInstance(jd, n, inst) }
config.earthDistanceN = func(jd float64, n int) float64 { return EarthNeptuneAwayNWithInstance(jd, n, inst) }
config.semidiameterN = func(jde float64, n int) float64 { return NeptuneSemidiameterNWithInstance(jde, n, inst) }
default:
return planetOccultationConfig{}, false
}
@@ -199,8 +195,9 @@ func planetOccultationCandidateGreatestTimes(
startTT, endTT, step float64,
config planetOccultationConfig,
observer *Observer,
safetyMarginArcsec float64,
) []float64 {
safetyMarginArcsec float64, instances ...*Instance) []float64 {
inst := instanceValue(instances)
if endTT <= startTT {
return nil
}
@@ -214,20 +211,20 @@ func planetOccultationCandidateGreatestTimes(
leftTT := scanStart
centerTT := math.Min(leftTT+step, scanEnd)
leftValue := planetOccultationExternalContactMetric(leftTT, config, observer, 8)
centerValue := planetOccultationExternalContactMetric(centerTT, config, observer, 8)
leftValue := planetOccultationExternalContactMetric(leftTT, config, observer, 8, inst)
centerValue := planetOccultationExternalContactMetric(centerTT, config, observer, 8, inst)
results := make([]float64, 0)
for centerTT < scanEnd {
rightTT := math.Min(centerTT+step, scanEnd)
rightValue := planetOccultationExternalContactMetric(rightTT, config, observer, 8)
rightValue := planetOccultationExternalContactMetric(rightTT, config, observer, 8, inst)
candidateLimit := planetOccultationCandidateLimitArcsec + safetyMarginArcsec
if finite(leftValue) && finite(centerValue) && finite(rightValue) &&
centerValue <= leftValue && centerValue <= rightValue && centerValue <= candidateLimit {
// 最小外接触度量同时包含动态月面和行星盘面,因此定义事件是否存在以及报告的掩甚时刻。
// The minimum outer-contact metric includes both dynamic disks and therefore defines event existence and the reported greatest instant.
greatestTT := planetOccultationMinimizeExternalMetric(leftTT, rightTT, config, observer)
greatestTT := planetOccultationMinimizeExternalMetric(leftTT, rightTT, config, observer, inst)
if greatestTT >= startTT && greatestTT <= endTT &&
planetOccultationLatitudePass(greatestTT, config, observer, safetyMarginArcsec) {
planetOccultationLatitudePass(greatestTT, config, observer, safetyMarginArcsec, inst) {
if len(results) == 0 || math.Abs(greatestTT-results[len(results)-1]) > 60.0/86400.0 {
results = append(results, greatestTT)
}
@@ -239,34 +236,38 @@ func planetOccultationCandidateGreatestTimes(
return results
}
func planetOccultationMinimizeExternalMetric(left, right float64, config planetOccultationConfig, observer *Observer) float64 {
func planetOccultationMinimizeExternalMetric(left, right float64, config planetOccultationConfig, observer *Observer, instances ...*Instance) float64 {
inst := instanceValue(instances)
if right <= left {
return left
}
const goldenRatio = 0.6180339887498949
x1 := right - goldenRatio*(right-left)
x2 := left + goldenRatio*(right-left)
f1 := planetOccultationExternalContactMetric(x1, config, observer, -1)
f2 := planetOccultationExternalContactMetric(x2, config, observer, -1)
f1 := planetOccultationExternalContactMetric(x1, config, observer, -1, inst)
f2 := planetOccultationExternalContactMetric(x2, config, observer, -1, inst)
for i := 0; i < 64 && right-left > planetOccultationRootToleranceDays; i++ {
if f1 > f2 {
left = x1
x1, f1 = x2, f2
x2 = left + goldenRatio*(right-left)
f2 = planetOccultationExternalContactMetric(x2, config, observer, -1)
f2 = planetOccultationExternalContactMetric(x2, config, observer, -1, inst)
} else {
right = x2
x2, f2 = x1, f1
x1 = right - goldenRatio*(right-left)
f1 = planetOccultationExternalContactMetric(x1, config, observer, -1)
f1 = planetOccultationExternalContactMetric(x1, config, observer, -1, inst)
}
}
return (left + right) / 2
}
func planetOccultationBestObserver(seedTT, startTT, endTT float64, config planetOccultationConfig) (float64, Observer, float64, bool) {
func planetOccultationBestObserver(seedTT, startTT, endTT float64, config planetOccultationConfig, instances ...*Instance) (float64, Observer, float64, bool) {
inst := instanceValue(instances)
frameAt := func(tt float64) (occultationPathFrame, bool) {
return planetOccultationPathFrameAt(tt, config)
return planetOccultationPathFrameAt(tt, config, inst)
}
searchStart := seedTT - occultationPathSearchSpanDays
searchEnd := seedTT + occultationPathSearchSpanDays
@@ -288,14 +289,14 @@ func planetOccultationBestObserver(seedTT, startTT, endTT float64, config planet
// outside the occultation, so it remains the last resort only.
point, pointOK = occultationPathTrackPointForFrame(greatestTT, frameAt, time.UTC)
if !pointOK {
point, pointOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, time.UTC)
point, pointOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, time.UTC, inst)
}
}
if !pointOK {
return 0, Observer{}, 0, false
}
observer := Observer{Longitude: point.Longitude, Latitude: point.Latitude}
state := planetOccultationStateAt(greatestTT, config, &observer, -1)
state := planetOccultationStateAt(greatestTT, config, &observer, -1, inst)
if !state.valid {
return 0, Observer{}, 0, false
}
@@ -307,14 +308,32 @@ func planetOccultationInfoAtGreatest(
config planetOccultationConfig,
observer Observer,
safetyMarginArcsec float64,
location *time.Location,
) (PlanetOccultationInfo, bool) {
if !planetOccultationLatitudePass(greatestTT, config, &observer, safetyMarginArcsec) {
location *time.Location, instances ...*Instance) (PlanetOccultationInfo, bool) {
inst := instanceValue(instances)
info, state, ok := planetOccultationGreatestInfo(greatestTT, config, observer, safetyMarginArcsec, location, inst)
if !ok || state.externalContactMetric > 0 {
return PlanetOccultationInfo{}, false
}
state := planetOccultationStateAt(greatestTT, config, &observer, -1)
if !state.valid || state.externalContactMetric > 0 {
return PlanetOccultationInfo{}, false
if math.Abs(state.externalContactMetric) <= planetOccultationGrazingToleranceArcsec {
info.Type = OccultationGrazing
info.ExternalImmersion = info.Greatest
info.ExternalEmersion = info.Greatest
info.ContactsComplete = true
return info, true
}
return planetOccultationCircularContacts(info, state, greatestTT, config, observer, location, inst)
}
// planetOccultationGreatestInfo 只填掩甚几何,不判断圆月面是否成掩、不求接触。
func planetOccultationGreatestInfo(greatestTT float64, config planetOccultationConfig, observer Observer, safetyMarginArcsec float64, location *time.Location, inst *Instance) (PlanetOccultationInfo, planetOccultationState, bool) {
if !planetOccultationLatitudePass(greatestTT, config, &observer, safetyMarginArcsec, inst) {
return PlanetOccultationInfo{}, planetOccultationState{}, false
}
state := planetOccultationStateAt(greatestTT, config, &observer, -1, inst)
if !state.valid {
return PlanetOccultationInfo{}, planetOccultationState{}, false
}
info := PlanetOccultationInfo{
Planet: config.planet,
@@ -330,16 +349,11 @@ func planetOccultationInfoAtGreatest(
MoonAzimuthAtGreatest: occultationAzimuth(greatestTT, observer, state.position.moonRA, state.position.moonDec),
}
info.VisibleAtGreatest = info.MoonAltitudeAtGreatest >= 0
return info, state, true
}
if math.Abs(state.externalContactMetric) <= planetOccultationGrazingToleranceArcsec {
info.Type = OccultationGrazing
info.ExternalImmersion = info.Greatest
info.ExternalEmersion = info.Greatest
info.ContactsComplete = true
return info, true
}
contactEvaluator := newPlanetOccultationContactEvaluator(config, observer)
func planetOccultationCircularContacts(info PlanetOccultationInfo, state planetOccultationState, greatestTT float64, config planetOccultationConfig, observer Observer, location *time.Location, inst *Instance) (PlanetOccultationInfo, bool) {
contactEvaluator := newPlanetOccultationContactEvaluator(config, observer, inst)
contactEvaluator.prime(greatestTT, state.movingDiskContactState(), state.valid)
externalImmersionTT, externalImmersionOK := planetOccultationContactWithEvaluator(greatestTT, -1, false, contactEvaluator)
externalEmersionTT, externalEmersionOK := planetOccultationContactWithEvaluator(greatestTT, 1, false, contactEvaluator)
@@ -366,12 +380,14 @@ func planetOccultationInfoAtGreatest(
return info, true
}
func planetMoonPositionAt(tt float64, config planetOccultationConfig, observer *Observer, n int) planetMoonPosition {
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, n)
func planetMoonPositionAt(tt float64, config planetOccultationConfig, observer *Observer, n int, instances ...*Instance) planetMoonPosition {
inst := instanceValue(instances)
moonRA, moonDec := HMoonGeocentricApparentRaDecNWithInstance(tt, n, inst)
planetRA, planetDec := config.apparentRaDecN(tt, n)
if observer != nil {
ut := TT2UTC(tt)
moonDistanceAU := HMoonAwayN(tt, n) / angularDiameterAstronomicalUnitKM
moonDistanceAU := HMoonAwayNWithInstance(tt, n, inst) / angularDiameterAstronomicalUnitKM
planetDistanceAU := config.earthDistanceN(tt, n)
moonRA, moonDec = TopocentricRaDec(moonRA, moonDec, observer.Latitude, observer.Longitude, ut, moonDistanceAU, observer.Height)
planetRA, planetDec = TopocentricRaDec(planetRA, planetDec, observer.Latitude, observer.Longitude, ut, planetDistanceAU, observer.Height)
@@ -387,12 +403,14 @@ func planetMoonPositionAt(tt float64, config planetOccultationConfig, observer *
}
}
func planetOccultationStateAt(tt float64, config planetOccultationConfig, observer *Observer, n int) planetOccultationState {
position := planetMoonPositionAt(tt, config, observer, n)
moonRadius := MoonSemidiameterN(tt, n)
func planetOccultationStateAt(tt float64, config planetOccultationConfig, observer *Observer, n int, instances ...*Instance) planetOccultationState {
inst := instanceValue(instances)
position := planetMoonPositionAt(tt, config, observer, n, inst)
moonRadius := MoonSemidiameterNWithInstance(tt, n, inst)
planetRadius := config.semidiameterN(tt, n)
if observer != nil {
moonRadius = moonTopocentricSemidiameterN(tt, *observer, n)
moonRadius = moonTopocentricSemidiameterN(tt, *observer, n, inst)
planetRadius = planetTopocentricSemidiameterN(tt, config, *observer, n)
}
if !position.valid || !finite(moonRadius) || !finite(planetRadius) || moonRadius <= planetRadius || planetRadius <= 0 {
@@ -432,16 +450,20 @@ func planetTopocentricSemidiameterN(tt float64, config planetOccultationConfig,
return angularSemidiameterArcsec(config.equatorialRadiusKM, distanceKM)
}
func planetOccultationExternalContactMetric(tt float64, config planetOccultationConfig, observer *Observer, n int) float64 {
state := planetOccultationStateAt(tt, config, observer, n)
func planetOccultationExternalContactMetric(tt float64, config planetOccultationConfig, observer *Observer, n int, instances ...*Instance) float64 {
inst := instanceValue(instances)
state := planetOccultationStateAt(tt, config, observer, n, inst)
if !state.valid {
return math.Inf(1)
}
return state.externalContactMetric
}
func planetOccultationLatitudePass(tt float64, config planetOccultationConfig, observer *Observer, safetyMarginArcsec float64) bool {
state := planetOccultationStateAt(tt, config, observer, -1)
func planetOccultationLatitudePass(tt float64, config planetOccultationConfig, observer *Observer, safetyMarginArcsec float64, instances ...*Instance) bool {
inst := instanceValue(instances)
state := planetOccultationStateAt(tt, config, observer, -1, inst)
if !state.valid {
return false
}
@@ -453,10 +475,11 @@ func planetOccultationLatitudePass(tt float64, config planetOccultationConfig, o
func newPlanetOccultationContactEvaluator(
config planetOccultationConfig,
observer Observer,
) *movingDiskContactEvaluator {
observer Observer, instances ...*Instance) *movingDiskContactEvaluator {
inst := instanceValue(instances)
return newMovingDiskContactEvaluator(func(tt float64) (movingDiskContactState, bool) {
state := planetOccultationStateAt(tt, config, &observer, -1)
state := planetOccultationStateAt(tt, config, &observer, -1, inst)
return state.movingDiskContactState(), state.valid
})
}
+24 -19
View File
@@ -72,28 +72,26 @@ type planetOccultationDiagramTime struct {
labels []string
}
// PlanetOccultationDiagram 为已求解的固定地点行星月掩计算以月心为原点的切平面轨迹。事件数据无效或不完整时,结果不含帧。
// PlanetOccultationDiagram computes a Moon-centered tangent-plane track for an already solved fixed-site planetary occultation. Invalid or incomplete event data produces a result without frames.
func PlanetOccultationDiagram(
// PlanetOccultationDiagramWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func PlanetOccultationDiagramWithInstance(
info PlanetOccultationInfo,
options PlanetOccultationDiagramOptions,
) PlanetOccultationDiagramResult {
options PlanetOccultationDiagramOptions, inst *Instance) PlanetOccultationDiagramResult {
options = normalizePlanetOccultationDiagramOptions(options)
result := PlanetOccultationDiagramResult{Occultation: info, StepDays: options.StepDays}
if !planetOccultationDiagramInputValid(info) {
return result
}
config, ok := planetOccultationConfigFor(info.Planet)
config, ok := planetOccultationConfigFor(info.Planet, inst)
if !ok {
return result
}
startTT := occultationTimeToTT(info.ExternalImmersion)
greatestTT := occultationTimeToTT(info.Greatest)
endTT := occultationTimeToTT(info.ExternalEmersion)
externalImmersionFrame, externalImmersionOK := planetOccultationDiagramFrameAt(startTT, config, info.Observer)
greatestFrame, greatestOK := planetOccultationDiagramFrameAt(greatestTT, config, info.Observer)
externalEmersionFrame, externalEmersionOK := planetOccultationDiagramFrameAt(endTT, config, info.Observer)
externalImmersionFrame, externalImmersionOK := planetOccultationDiagramFrameAt(startTT, config, info.Observer, inst)
greatestFrame, greatestOK := planetOccultationDiagramFrameAt(greatestTT, config, info.Observer, inst)
externalEmersionFrame, externalEmersionOK := planetOccultationDiagramFrameAt(endTT, config, info.Observer, inst)
if !externalImmersionOK || !greatestOK || !externalEmersionOK {
return result
}
@@ -103,10 +101,10 @@ func PlanetOccultationDiagram(
)
if info.HasInternalContacts {
internalImmersionFrame, internalImmersionOK = planetOccultationDiagramFrameAt(
occultationTimeToTT(info.InternalImmersion), config, info.Observer,
occultationTimeToTT(info.InternalImmersion), config, info.Observer, inst,
)
internalEmersionFrame, internalEmersionOK = planetOccultationDiagramFrameAt(
occultationTimeToTT(info.InternalEmersion), config, info.Observer,
occultationTimeToTT(info.InternalEmersion), config, info.Observer, inst,
)
}
if (info.HasInternalContacts && (!internalImmersionOK || !internalEmersionOK)) ||
@@ -120,7 +118,7 @@ func PlanetOccultationDiagram(
result.StepDays = stepDays
result.Frames = make([]PlanetOccultationDiagramFrame, 0, len(times))
for _, item := range times {
frame, frameOK := planetOccultationDiagramFrameAt(item.jde, config, info.Observer)
frame, frameOK := planetOccultationDiagramFrameAt(item.jde, config, info.Observer, inst)
if !frameOK {
return PlanetOccultationDiagramResult{Occultation: info, StepDays: stepDays}
}
@@ -142,8 +140,10 @@ func planetOccultationDiagramMatchesInfo(
!finite(info.PlanetSemidiameterArcsec) || info.PlanetSemidiameterArcsec <= 0 {
return false
}
if math.Abs(externalImmersion.SeparationArcsec-externalImmersion.MoonRadiusArcsec-externalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec ||
math.Abs(externalEmersion.SeparationArcsec-externalEmersion.MoonRadiusArcsec-externalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec {
// 已按月缘剖面收敛的接触不在圆月面上,圆月面接触判据只适用于未修正的结果。
checkContacts := !info.LunarLimbApplied
if checkContacts && (math.Abs(externalImmersion.SeparationArcsec-externalImmersion.MoonRadiusArcsec-externalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec ||
math.Abs(externalEmersion.SeparationArcsec-externalEmersion.MoonRadiusArcsec-externalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec) {
return false
}
if math.Abs(greatest.SeparationArcsec-info.MinimumSeparationArcsec) > planetOccultationDiagramGeometryArcsec ||
@@ -153,6 +153,10 @@ func planetOccultationDiagramMatchesInfo(
return false
}
// 月缘结果的食型按剖面判定,圆月面掩甚间隙不再适用。
if info.LunarLimbApplied {
return info.Type == OccultationPartial || info.Type == OccultationTotal || info.Type == OccultationGrazing
}
externalMetric := greatest.SeparationArcsec - greatest.MoonRadiusArcsec - greatest.PlanetRadiusArcsec
internalMetric := greatest.SeparationArcsec - greatest.MoonRadiusArcsec + greatest.PlanetRadiusArcsec
switch info.Type {
@@ -164,8 +168,8 @@ func planetOccultationDiagramMatchesInfo(
math.Abs(startTT-greatestTT) <= planetOccultationDiagramContactTimeDays &&
math.Abs(endTT-greatestTT) <= planetOccultationDiagramContactTimeDays
case OccultationTotal:
if math.Abs(internalImmersion.SeparationArcsec-internalImmersion.MoonRadiusArcsec+internalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec ||
math.Abs(internalEmersion.SeparationArcsec-internalEmersion.MoonRadiusArcsec+internalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec {
if checkContacts && (math.Abs(internalImmersion.SeparationArcsec-internalImmersion.MoonRadiusArcsec+internalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec ||
math.Abs(internalEmersion.SeparationArcsec-internalEmersion.MoonRadiusArcsec+internalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec) {
return false
}
return externalMetric < -planetOccultationGrazingToleranceArcsec &&
@@ -291,9 +295,10 @@ func planetOccultationDiagramLabelPriority(labels []string) int {
func planetOccultationDiagramFrameAt(
tt float64,
config planetOccultationConfig,
observer Observer,
) (PlanetOccultationDiagramFrame, bool) {
state := planetOccultationStateAt(tt, config, &observer, -1)
observer Observer, instances ...*Instance) (PlanetOccultationDiagramFrame, bool) {
inst := instanceValue(instances)
state := planetOccultationStateAt(tt, config, &observer, -1, inst)
if !state.valid {
return PlanetOccultationDiagramFrame{}, false
}
+11 -9
View File
@@ -4,6 +4,8 @@ import (
"math"
"sort"
"time"
"b612.me/astro/internal/geodata"
)
const (
@@ -730,9 +732,9 @@ func planetOccultationHorizonArc(
from, to := segment[len(segment)-1], segment[0]
unitAt := func(point OccultationPathPoint) occultationPathVector {
fixed := occultationStationSurfaceVector(point.Longitude, point.Latitude)
return occultationPathVector{x: fixed.x / occultationPathEarthEquatorialRadiusKM,
y: fixed.y / occultationPathEarthEquatorialRadiusKM,
z: fixed.z / (occultationPathEarthEquatorialRadiusKM * occultationPathEarthPolarRatio)}
return occultationPathVector{x: fixed.x / geodata.EarthEquatorialRadiusKM,
y: fixed.y / geodata.EarthEquatorialRadiusKM,
z: fixed.z / (geodata.EarthEquatorialRadiusKM * occultationPathEarthPolarRatio)}
}
moonFixed := occultationPathEarthFixedVector(tt, frame.moon)
normal := occultationPathUnit(occultationPathVector{x: moonFixed.x, y: moonFixed.y, z: moonFixed.z / occultationPathEarthPolarRatio})
@@ -772,7 +774,7 @@ func planetOccultationHorizonArc(
unit := occultationPathAdd(occultationPathScale(normal, centerDistance),
occultationPathScale(occultationPathAdd(occultationPathScale(first, math.Cos(angle)),
occultationPathScale(second, math.Sin(angle))), radius))
fixed := occultationPathScale(unit, occultationPathEarthEquatorialRadiusKM)
fixed := occultationPathScale(unit, geodata.EarthEquatorialRadiusKM)
fixed.z *= occultationPathEarthPolarRatio
middle := from
middle.Longitude, middle.Latitude = occultationStationGeodetic(fixed)
@@ -822,13 +824,13 @@ func planetOccultationCenterCap(
if math.Abs(cosine) < 1e-6 {
cosine = 1e-6
}
latitudeOffset := radiusKM / occultationPathEarthEquatorialRadiusKM * degreesPerRadian
latitudeOffset := radiusKM / geodata.EarthEquatorialRadiusKM * degreesPerRadian
longitudeOffset := latitudeOffset / cosine
cap := make([]OccultationPathPoint, pointCount+1)
for index := 0; index < pointCount; index++ {
angle := 2 * math.Pi * float64(index) / float64(pointCount)
cap[index] = center
cap[index].Longitude = normalizeLongitude(center.Longitude + longitudeOffset*math.Cos(angle))
cap[index].Longitude = geodata.NormalizeLongitude(center.Longitude + longitudeOffset*math.Cos(angle))
cap[index].Latitude = math.Max(-90, math.Min(90, center.Latitude+latitudeOffset*math.Sin(angle)))
cap[index].MoonAltitude = center.MoonAltitude
cap[index].WidthKM = 0
@@ -872,8 +874,8 @@ func planetOccultationPointSegmentDistanceKM(
point, start, end OccultationPathPoint,
) float64 {
latitude := point.Latitude * math.Pi / 180
scaleX := math.Cos(latitude) * occultationPathEarthEquatorialRadiusKM * math.Pi / 180
scaleY := occultationPathEarthEquatorialRadiusKM * math.Pi / 180
scaleX := math.Cos(latitude) * geodata.EarthEquatorialRadiusKM * math.Pi / 180
scaleY := geodata.EarthEquatorialRadiusKM * math.Pi / 180
x := func(value OccultationPathPoint) float64 {
return math.Remainder(value.Longitude-point.Longitude, 360) * scaleX
}
@@ -934,7 +936,7 @@ func planetOccultationHorizonCircle(
if count < 3 {
return nil
}
equatorialRadius := occultationPathEarthEquatorialRadiusKM
equatorialRadius := geodata.EarthEquatorialRadiusKM
polarRadius := equatorialRadius * occultationPathEarthPolarRatio
scaledMoon := occultationPathVector{
x: frame.moon.x / equatorialRadius,
+67 -54
View File
@@ -4,6 +4,8 @@ import (
"math"
"sort"
"time"
"b612.me/astro/internal/geodata"
)
const (
@@ -30,6 +32,8 @@ type planetOccultationFrameCacheEntry struct {
}
type planetOccultationEventCache struct {
inst *Instance
config planetOccultationConfig
states map[uint64]planetOccultationEphemerisState
outerFrames map[uint64]planetOccultationFrameCacheEntry
@@ -39,8 +43,10 @@ type planetOccultationEventCache struct {
local *planetOccultationLocalEphemeris
}
func newPlanetOccultationEventCache(config planetOccultationConfig) *planetOccultationEventCache {
cache := &planetOccultationEventCache{
func newPlanetOccultationEventCache(config planetOccultationConfig, instances ...*Instance) *planetOccultationEventCache {
inst := instanceValue(instances)
cache := &planetOccultationEventCache{inst: inst,
config: config,
states: make(map[uint64]planetOccultationEphemerisState),
outerFrames: make(map[uint64]planetOccultationFrameCacheEntry),
@@ -57,12 +63,9 @@ func newPlanetOccultationEventCache(config planetOccultationConfig) *planetOccul
return cache
}
// FindPlanetOccultationPaths 搜索有限盘面行星月掩的全球外接触和内接触掩带。
// 查询窗口按全球几何掩甚点选择事件;端点容差 10 ms 与数值根精度一致。求解成功时,每条路径扩展到完整全球起止点。
// FindPlanetOccultationPaths searches the global outer- and inner-contact footprints of one finite-disk planet.
// The query window selects events by global geometric greatest, with a 10 ms endpoint tolerance matching the numerical root precision. Each returned path expands to its complete global start and end when solved.
func FindPlanetOccultationPaths(start, end time.Time, planet OccultationPlanet,
options OccultationPathOptions) ([]PlanetOccultationPath, error) {
// FindPlanetOccultationPathsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func FindPlanetOccultationPathsWithInstance(start, end time.Time, planet OccultationPlanet,
options OccultationPathOptions, inst *Instance) ([]PlanetOccultationPath, error) {
if err := validateOccultationTimeRange(start, end); err != nil {
return nil, err
}
@@ -73,18 +76,18 @@ func FindPlanetOccultationPaths(start, end time.Time, planet OccultationPlanet,
return nil, err
}
config, _ := planetOccultationConfigFor(planet)
config, _ := planetOccultationConfigFor(planet, inst)
options = normalizeOccultationPathOptions(options)
startTT := occultationTimeToTT(start)
endTT := occultationTimeToTT(end)
candidateStartTT := startTT - occultationPathSearchSpanDays
candidateEndTT := endTT + occultationPathSearchSpanDays
candidates := planetOccultationCandidateGreatestTimes(
candidateStartTT, candidateEndTT, planetOccultationDefaultStepDays, config, nil, 0,
candidateStartTT, candidateEndTT, planetOccultationDefaultStepDays, config, nil, 0, inst,
)
paths := make([]PlanetOccultationPath, 0, len(candidates))
for _, seedTT := range candidates {
path, ok, err := planetOccultationPathAtSeed(seedTT, config, options, start, end)
path, ok, err := planetOccultationPathAtSeed(seedTT, config, options, start, end, inst)
if err != nil {
return nil, err
}
@@ -106,10 +109,11 @@ func planetOccultationPathAtSeed(
seedTT float64,
config planetOccultationConfig,
options OccultationPathOptions,
selectionStart, selectionEnd time.Time,
) (PlanetOccultationPath, bool, error) {
selectionStart, selectionEnd time.Time, instances ...*Instance) (PlanetOccultationPath, bool, error) {
inst := instanceValue(instances)
location := selectionStart.Location()
cache := newPlanetOccultationEventCache(config)
cache := newPlanetOccultationEventCache(config, inst)
cache.prepareLocalEphemeris(seedTT)
frameAt := cache.outerFrameAt
totalFrameAt := cache.totalFrameAt
@@ -182,14 +186,14 @@ func planetOccultationPathAtSeed(
return PlanetOccultationPath{}, false, ErrOccultationPathSamplingLimit
}
start := occultationPathBoundaryEndpointForFrame(outerStart, frameAt, location, 1)
end := occultationPathBoundaryEndpointForFrame(outerEnd, frameAt, location, -1)
start := occultationPathBoundaryEndpointForFrame(outerStart, frameAt, location, 1, inst)
end := occultationPathBoundaryEndpointForFrame(outerEnd, frameAt, location, -1, inst)
if !start.valid || !end.valid {
return PlanetOccultationPath{}, false, nil
}
exactFrameAt, exactTotalFrameAt := frameAt, totalFrameAt
if options.Algorithm != OccultationPathAlgorithmExact {
optimized := newPlanetOccultationEventCache(config)
optimized := newPlanetOccultationEventCache(config, inst)
optimized.preparePathEphemeris(seedTT, options.Algorithm)
if optimized.local.dense {
cache = optimized
@@ -197,7 +201,7 @@ func planetOccultationPathAtSeed(
}
}
centerLine, northern, southern, err := planetOccultationPathSamples(
outerStart, outerEnd, centerStart, centerEnd, hasCenter, greatestTT, frameAt, options, location,
outerStart, outerEnd, centerStart, centerEnd, hasCenter, greatestTT, frameAt, exactFrameAt, options, location,
)
if err != nil {
return PlanetOccultationPath{}, false, err
@@ -228,6 +232,10 @@ func planetOccultationPathAtSeed(
path.SouthernLimit = occultationStationCorrectLimitSeries(
path.SouthernLimit, frameAt, cache.riseSetContextAt, false, location,
)
var northApplied, southApplied bool
path.NorthernLimit, northApplied = occultationLimbCorrectLimitSeries(path.NorthernLimit, cache.riseSetContextAt, options.LunarLimb, location, inst)
path.SouthernLimit, southApplied = occultationLimbCorrectLimitSeries(path.SouthernLimit, cache.riseSetContextAt, options.LunarLimb, location, inst)
path.LunarLimbApplied = northApplied || southApplied
path.GreatestLimitSeparationKM, _ = occultationPathLimitSeparations(
path.NorthernLimit, path.SouthernLimit, greatestTT,
)
@@ -282,8 +290,8 @@ func planetOccultationPathAtSeed(
if !hasTotal {
return path, true, nil
}
totalStart := occultationPathBoundaryEndpointForFrame(totalStartTT, exactTotalFrameAt, location, 1)
totalEnd := occultationPathBoundaryEndpointForFrame(totalEndTT, exactTotalFrameAt, location, -1)
totalStart := occultationPathBoundaryEndpointForFrame(totalStartTT, exactTotalFrameAt, location, 1, inst)
totalEnd := occultationPathBoundaryEndpointForFrame(totalEndTT, exactTotalFrameAt, location, -1, inst)
_, _, totalWidth, totalWidthOK := occultationPathLimitsAndWidthForFrame(greatestTT, exactTotalFrameAt)
if !totalStart.valid || !totalEnd.valid || !totalWidthOK || totalWidth <= 0 {
return path, true, nil
@@ -464,14 +472,14 @@ func planetOccultationPathSamples(
centerStartTT, centerEndTT float64,
hasCenter bool,
greatestTT float64,
frameAt occultationPathFrameFunc,
frameAt, exactFrameAt occultationPathFrameFunc,
options OccultationPathOptions,
location *time.Location,
) ([]OccultationPathPoint, []OccultationPathPoint, []OccultationPathPoint, error) {
var centerLine []OccultationPathPoint
if hasCenter {
var err error
centerLine, err = occultationPathCenterSamplesForFrame(centerStartTT, centerEndTT, greatestTT, frameAt, options, location)
centerLine, err = occultationPathCenterSamplesForFrame(centerStartTT, centerEndTT, greatestTT, frameAt, exactFrameAt, options, location)
if err != nil {
return nil, nil, nil, err
}
@@ -613,11 +621,6 @@ func appendOccultationPathBoundaryPairSegment(
)
}
const (
occultationPathWebMercatorRadiusKM = 6378.1366
occultationPathWebMercatorMaxLat = 85.05112878
)
func occultationPathBoundaryPairWebMercatorSpacing(
first, second occultationPathBoundaryPairSample,
) float64 {
@@ -656,13 +659,10 @@ func occultationPathWebMercatorPointSpacing(
) float64 {
firstLongitude, firstLatitude := occultationPathGeodetic(firstTT, firstVector)
secondLongitude, secondLatitude := occultationPathGeodetic(secondTT, secondVector)
firstLatitude = math.Max(-occultationPathWebMercatorMaxLat, math.Min(occultationPathWebMercatorMaxLat, firstLatitude))
secondLatitude = math.Max(-occultationPathWebMercatorMaxLat, math.Min(occultationPathWebMercatorMaxLat, secondLatitude))
longitudeDelta := math.Remainder(secondLongitude-firstLongitude, 360) * math.Pi / 180
firstY := math.Log(math.Tan(math.Pi/4 + firstLatitude*math.Pi/360))
secondY := math.Log(math.Tan(math.Pi/4 + secondLatitude*math.Pi/360))
latitudeDelta := secondY - firstY
return occultationPathWebMercatorRadiusKM * math.Hypot(longitudeDelta, latitudeDelta)
return geodata.WebMercatorDistanceKM(
geodata.GeoPoint{Longitude: firstLongitude, Latitude: firstLatitude},
geodata.GeoPoint{Longitude: secondLongitude, Latitude: secondLatitude},
)
}
func occultationPathContourBoundaryPairAt(
@@ -1074,7 +1074,7 @@ func occultationPathOrientBoundarySamples(
func occultationPathCenterSamplesForFrame(
startTT, endTT, greatestTT float64,
frameAt occultationPathFrameFunc,
frameAt, exactFrameAt occultationPathFrameFunc,
options OccultationPathOptions,
location *time.Location,
) ([]OccultationPathPoint, error) {
@@ -1083,6 +1083,10 @@ func occultationPathCenterSamplesForFrame(
points := make([]OccultationPathPoint, 0, len(times))
for _, tt := range times {
point, ok := occultationPathCenterPointForFrame(tt, frameAt, location)
// 中心窗口端点由精确帧求得,插值帧在端点附近可能判无交点,此时回退精确帧。
if !ok && exactFrameAt != nil {
point, ok = occultationPathCenterPointForFrame(tt, exactFrameAt, location)
}
if ok && (len(points) == 0 || point.Time.After(points[len(points)-1].Time)) {
points = append(points, point)
}
@@ -1156,23 +1160,29 @@ func appendOccultationPathSegmentForFrame(
return appendOccultationPathSegmentForFrame(points, mid, end, frameAt, targetSpacingKM, location, depth+1)
}
func planetOccultationPathFrameAt(tt float64, config planetOccultationConfig) (occultationPathFrame, bool) {
return planetOccultationContactPathFrameAt(tt, config, false)
func planetOccultationPathFrameAt(tt float64, config planetOccultationConfig, instances ...*Instance) (occultationPathFrame, bool) {
inst := instanceValue(instances)
return planetOccultationContactPathFrameAt(tt, config, false, inst)
}
func planetOccultationTotalPathFrameAt(tt float64, config planetOccultationConfig) (occultationPathFrame, bool) {
return planetOccultationContactPathFrameAt(tt, config, true)
}
func planetOccultationContactPathFrameAt(tt float64, config planetOccultationConfig, total bool) (occultationPathFrame, bool) {
state := planetOccultationEphemerisStateAt(tt, config)
func planetOccultationContactPathFrameAt(tt float64, config planetOccultationConfig, total bool, instances ...*Instance) (occultationPathFrame, bool) {
inst := instanceValue(instances)
state := planetOccultationEphemerisStateAt(tt, config, inst)
return planetOccultationContactPathFrameFromState(state, config, total)
}
func planetOccultationEphemerisStateAt(tt float64, config planetOccultationConfig) planetOccultationEphemerisState {
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1)
moonDistance := HMoonAwayN(tt, -1)
planetRA, planetDec, planetDistanceAU := planetOccultationApparentPositionAndDistanceN(tt, config, -1)
func planetOccultationEphemerisStateAt(tt float64, config planetOccultationConfig, instances ...*Instance) planetOccultationEphemerisState {
inst := instanceValue(instances)
moonRA, moonDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
moonDistance := HMoonAwayNWithInstance(tt, -1, inst)
planetRA, planetDec, planetDistanceAU := planetOccultationApparentPositionAndDistanceN(tt, config, -1, inst)
planetDistance := planetDistanceAU * occultationPathAstronomicalUnitKM
return planetOccultationEphemerisState{
moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistance,
@@ -1185,9 +1195,10 @@ func planetOccultationEphemerisStateAt(tt float64, config planetOccultationConfi
func planetOccultationApparentPositionAndDistanceN(
tt float64,
config planetOccultationConfig,
n int,
) (float64, float64, float64) {
position, _, distanceAU := planetApparentGeocentricPositionAndDistanceN(config.planetIndex, tt, n)
n int, instances ...*Instance) (float64, float64, float64) {
inst := instanceValue(instances)
position, _, distanceAU := planetApparentGeocentricPositionAndDistanceN(config.planetIndex, tt, n, inst)
ra, dec := planetApparentRaDecFromLoBo(tt, position.lo, position.bo)
return ra, dec, distanceAU
}
@@ -1253,7 +1264,7 @@ func (cache *planetOccultationEventCache) stateAt(tt float64) planetOccultationE
state, interpolated = cache.local.stateAt(tt)
}
if !interpolated {
state = planetOccultationEphemerisStateAt(tt, cache.config)
state = planetOccultationEphemerisStateAt(tt, cache.config, cache.inst)
}
cache.states[key] = state
return state
@@ -1261,7 +1272,7 @@ func (cache *planetOccultationEventCache) stateAt(tt float64) planetOccultationE
func (cache *planetOccultationEventCache) prepareLocalEphemeris(center float64) {
if cache.local == nil {
cache.local = newPlanetOccultationLocalEphemeris(center, cache.config)
cache.local = newPlanetOccultationLocalEphemeris(center, cache.config, cache.inst)
}
}
@@ -1353,8 +1364,9 @@ func occultationPathBoundaryEndpointForFrame(
tt float64,
frameAt occultationPathFrameFunc,
location *time.Location,
direction int,
) occultationPathEndpoint {
direction int, instances ...*Instance) occultationPathEndpoint {
inst := instanceValue(instances)
if _, ok := frameAt(tt); !ok {
return occultationPathEndpoint{}
}
@@ -1366,7 +1378,7 @@ func occultationPathBoundaryEndpointForFrame(
}
vector, _, valid := occultationPathBoundaryTangent(frame)
if valid {
return occultationPathEndpoint{point: occultationPathPointFromVector(candidateTT, vector, 0, location), valid: true}
return occultationPathEndpoint{point: occultationPathPointFromVector(candidateTT, vector, 0, location, inst), valid: true}
}
}
return occultationPathEndpoint{}
@@ -1411,8 +1423,9 @@ func occultationPathCenterPointForFrameWithoutWidth(
func occultationPathBoundaryPointForFrame(
tt float64,
frameAt occultationPathFrameFunc,
location *time.Location,
) (OccultationPathPoint, bool) {
location *time.Location, instances ...*Instance) (OccultationPathPoint, bool) {
inst := instanceValue(instances)
frame, ok := frameAt(tt)
if !ok {
return OccultationPathPoint{}, false
@@ -1422,7 +1435,7 @@ func occultationPathBoundaryPointForFrame(
return OccultationPathPoint{}, false
}
north, south, limitsOK := occultationPathScannedLimitsAtFrame(tt, frame)
return occultationPathPointFromVector(tt, point, occultationPathBoundaryWidth(north, south, limitsOK), location), true
return occultationPathPointFromVector(tt, point, occultationPathBoundaryWidth(north, south, limitsOK), location, inst), true
}
func occultationPathTrackPointForFrame(
+4 -2
View File
@@ -5,6 +5,8 @@ import (
"strings"
"testing"
"time"
"b612.me/astro/internal/geodata"
)
func TestPlanetOccultationCombinedPositionMatchesSeparateEphemerides(t *testing.T) {
@@ -490,7 +492,7 @@ func TestPlanetOccultationInnerConeUsesSignedTargetRadius(t *testing.T) {
func TestOccultationPathBoundaryTangentFindsBetweenSamples(t *testing.T) {
const boundaryRadiusKM = 1737.4
theta := math.Pi / float64(occultationPathBoundaryScanPoints)
offset := occultationPathEarthEquatorialRadiusKM + boundaryRadiusKM - 0.01
offset := geodata.EarthEquatorialRadiusKM + boundaryRadiusKM - 0.01
moon := occultationPathVector{
x: 384000,
y: -offset * math.Cos(theta),
@@ -517,7 +519,7 @@ func TestOccultationPathBoundaryTangentFindsBetweenSamples(t *testing.T) {
}
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
ellipsoidResidual := point.x*point.x + point.y*point.y + point.z*point.z/polarRatioSquared -
occultationPathEarthEquatorialRadiusKM*occultationPathEarthEquatorialRadiusKM
geodata.EarthEquatorialRadiusKM*geodata.EarthEquatorialRadiusKM
if math.Abs(ellipsoidResidual) > 1e-3 {
t.Fatalf("tangent point ellipsoid residual = %.9f", ellipsoidResidual)
}
+6 -4
View File
@@ -4,6 +4,8 @@ import (
"math"
"testing"
"time"
"b612.me/astro/internal/geodata"
)
func TestPlanetOccultationSupportsAllPlanetTargets(t *testing.T) {
@@ -103,9 +105,9 @@ func TestPlanetOccultationUsesStationMoonDistanceForRadius(t *testing.T) {
t.Fatal("Saturn occultation config is unavailable")
}
moonRA, _ := HMoonGeocentricApparentRaDecN(tt, -1)
subMoonLongitude := normalizeLongitude180(moonRA - ApparentSiderealTime(TT2UTC(tt))*15)
subMoonLongitude := geodata.NormalizeLongitude(moonRA - ApparentSiderealTime(TT2UTC(tt))*15)
near := Observer{Longitude: subMoonLongitude, Latitude: 0}
far := Observer{Longitude: normalizeLongitude180(subMoonLongitude + 180), Latitude: 0}
far := Observer{Longitude: geodata.NormalizeLongitude(subMoonLongitude + 180), Latitude: 0}
nearState := planetOccultationStateAt(tt, config, &near, -1)
farState := planetOccultationStateAt(tt, config, &far, -1)
if !nearState.valid || !farState.valid {
@@ -128,9 +130,9 @@ func TestPlanetOccultationUsesStationPlanetDistanceForRadius(t *testing.T) {
t.Fatal("Mercury occultation config is unavailable")
}
planetRA, _ := config.apparentRaDecN(tt, -1)
subPlanetLongitude := normalizeLongitude180(planetRA - ApparentSiderealTime(TT2UTC(tt))*15)
subPlanetLongitude := geodata.NormalizeLongitude(planetRA - ApparentSiderealTime(TT2UTC(tt))*15)
near := Observer{Longitude: subPlanetLongitude, Latitude: 0}
far := Observer{Longitude: normalizeLongitude180(subPlanetLongitude + 180), Latitude: 0}
far := Observer{Longitude: geodata.NormalizeLongitude(subPlanetLongitude + 180), Latitude: 0}
nearState := planetOccultationStateAt(tt, config, &near, -1)
farState := planetOccultationStateAt(tt, config, &far, -1)
if !nearState.valid || !farState.valid {
+6 -6
View File
@@ -3,6 +3,8 @@ package basic
import (
"math"
"time"
"b612.me/astro/internal/geodata"
)
const (
@@ -26,8 +28,6 @@ const (
occultationRiseSetEvaluationCacheMaximumEntries = 16384
occultationRiseSetTimeEpsilonDays = 1e-8
occultationRiseSetTargetSpacingKM = 100.0
occultationTopocentricEarthRadiusKM = 6378.14
occultationTopocentricEarthPolarRadiusKM = 6356.755
)
// 站心赤经赤纬的历史公式使用固定地平视差常数;保留该尺度可使向量算法与既有结果数值等价。
@@ -372,7 +372,7 @@ func (context occultationRiseSetContext) moonHorizonResidual(longitude, latitude
sinTheta, cosTheta := math.Sincos(theta)
moonDotZenith := cosLatitude*(context.moon.positionKM.x*cosTheta+context.moon.positionKM.y*sinTheta) +
context.moon.positionKM.z*sinLatitude
polarRatio := occultationTopocentricEarthPolarRadiusKM / occultationTopocentricEarthRadiusKM
polarRatio := topocentricEarthPolarRadiusKM / topocentricEarthRadiusKM
shapeDotZenith := math.Sqrt(cosLatitude*cosLatitude + polarRatio*polarRatio*sinLatitude*sinLatitude)
residual := moonDotZenith - occultationLegacyParallaxRadiusKM*shapeDotZenith
return residual, finite(residual)
@@ -385,7 +385,7 @@ func occultationRiseSetObserverVectors(
theta := (siderealDegrees + longitude) * rad
sinLatitude, cosLatitude := math.Sincos(latitudeRad)
sinTheta, cosTheta := math.Sincos(theta)
polarRatio := occultationTopocentricEarthPolarRadiusKM / occultationTopocentricEarthRadiusKM
polarRatio := topocentricEarthPolarRadiusKM / topocentricEarthRadiusKM
u := math.Atan(polarRatio * math.Tan(latitudeRad))
sinU, cosU := math.Sincos(u)
shape := occultationPathVector{
@@ -395,7 +395,7 @@ func occultationRiseSetObserverVectors(
}
zenith := occultationPathVector{x: cosLatitude * cosTheta, y: cosLatitude * sinTheta, z: sinLatitude}
return occultationPathScale(shape, occultationLegacyParallaxRadiusKM),
occultationPathScale(shape, occultationTopocentricEarthRadiusKM), zenith
occultationPathScale(shape, topocentricEarthRadiusKM), zenith
}
func occultationRiseSetTopocentricDirection(
@@ -636,7 +636,7 @@ func (evaluation occultationRiseSetEvaluation) pointsAt(
return result
}
gst := ApparentSiderealTime(TT2UT1(evaluation.tt)) * 15
centerLongitude := normalizeLongitude(evaluation.center.moonRA - gst)
centerLongitude := geodata.NormalizeLongitude(evaluation.center.moonRA - gst)
centerLatitude := evaluation.center.moonDec
appendRoots := func(greatest bool) {
// Keep the historical geographic refinement for ordinary latitudes; it
+6 -4
View File
@@ -3,6 +3,8 @@ package basic
import (
"math"
"time"
"b612.me/astro/internal/geodata"
)
func refineOccultationRiseSetFold(
@@ -182,7 +184,7 @@ func occultationRiseSetFoldExtremumAt(
}
func occultationRiseSetHorizonCenter(context occultationRiseSetContext) (float64, float64) {
return normalizeLongitude(context.moonRA - context.siderealDegrees), context.moonDec
return geodata.NormalizeLongitude(context.moonRA - context.siderealDegrees), context.moonDec
}
func occultationRiseSetHorizonPoint(
@@ -307,7 +309,7 @@ func occultationRiseSetCirclePointFromTangent(center, tangent [3]float64, radius
center[1]*cosRadius + tangent[1]*sinRadius,
center[2]*cosRadius + tangent[2]*sinRadius,
}
return normalizeLongitude(math.Atan2(point[1], point[0]) / rad),
return geodata.NormalizeLongitude(math.Atan2(point[1], point[0]) / rad),
math.Asin(math.Max(-1, math.Min(1, point[2]))) / rad
}
@@ -385,7 +387,7 @@ func refineOccultationRiseSetFoldPoint(
for index := range coordinates {
coordinates[index] += delta[index]
}
coordinates[0] = normalizeLongitude(coordinates[0])
coordinates[0] = geodata.NormalizeLongitude(coordinates[0])
if coordinates[1] <= -89.999999 || coordinates[1] >= 89.999999 {
return OccultationPathPoint{}, false
}
@@ -477,7 +479,7 @@ func occultationRiseSetPhaseMidpoint(
); ok && occultationRiseSetRefinementPointIsContinuous(start, middle, end) {
return middle, true
}
longitude := normalizeLongitude(start.Longitude + math.Remainder(end.Longitude-start.Longitude, 360)/2)
longitude := geodata.NormalizeLongitude(start.Longitude + math.Remainder(end.Longitude-start.Longitude, 360)/2)
latitude := (start.Latitude + end.Latitude) / 2
evaluation := cache.evaluation(tt)
longitude, latitude, ok := riseSetRefineGeographicRoot(longitude, latitude, func(lon, lat float64) (float64, float64, bool) {
+25 -5
View File
@@ -4,6 +4,8 @@ import (
"math"
"sort"
"time"
"b612.me/astro/internal/geodata"
)
func appendOccultationRiseSetSamples(
@@ -485,7 +487,7 @@ func occultationRiseSetRecoverNarrowPhaseJunction(
if !middleOK || !occultationRiseSetDirectionMatches(evaluation, middle, direction) {
outside = OccultationPathPoint{
Time: occultationTTToLocation(middleTT, location),
Longitude: normalizeLongitude(
Longitude: geodata.NormalizeLongitude(
inside.Longitude + math.Remainder(outside.Longitude-inside.Longitude, 360)/2,
),
Latitude: (inside.Latitude + outside.Latitude) / 2,
@@ -506,6 +508,24 @@ func occultationRiseSetRecoverNarrowPhaseJunction(
if math.Abs(outsideContact) < math.Abs(insideContact) {
secondary = outside
}
// 时间区间收缩后两地平根仍差求根精度,沿短弦插值接触零点而不是取较优端点。
if denominator := insideContact - outsideContact; denominator != 0 {
weight := insideContact / denominator
insideTT := occultationTimeToTT(inside.Time)
interpolatedTT := insideTT + (occultationTimeToTT(outside.Time)-insideTT)*weight
interpolated := OccultationPathPoint{
Time: occultationTTToLocation(interpolatedTT, location),
Longitude: geodata.NormalizeLongitude(
inside.Longitude + math.Remainder(outside.Longitude-inside.Longitude, 360)*weight,
),
Latitude: inside.Latitude + (outside.Latitude-inside.Latitude)*weight,
}
state := cache.evaluation(interpolatedTT).center.stateAt(interpolated.Longitude, interpolated.Latitude)
if state.valid && finite(state.contactMetric) &&
math.Abs(state.contactMetric) < math.Min(math.Abs(insideContact), math.Abs(outsideContact)) {
secondary = interpolated
}
}
secondaryTT := occultationTimeToTT(secondary.Time)
secondaryEvaluation := cache.evaluation(secondaryTT)
secondaryState := secondaryEvaluation.center.stateAt(secondary.Longitude, secondary.Latitude)
@@ -624,7 +644,7 @@ func occultationRiseSetMidpoint(first, second OccultationPathPoint) OccultationP
secondTT := occultationTimeToTT(second.Time)
return OccultationPathPoint{
Time: occultationTTToLocation((firstTT+secondTT)/2, first.Time.Location()),
Longitude: normalizeLongitude(
Longitude: geodata.NormalizeLongitude(
first.Longitude + math.Remainder(second.Longitude-first.Longitude, 360)/2,
),
Latitude: (first.Latitude + second.Latitude) / 2,
@@ -672,7 +692,7 @@ func completeOccultationRiseSetDirectionJunctions(
riseEndpoint.point, setEndpoint.point, phase == RiseSetPhaseGreatest, location, cache,
)
if !ok {
longitude := normalizeLongitude(riseEndpoint.point.Longitude + math.Remainder(setEndpoint.point.Longitude-riseEndpoint.point.Longitude, 360)/2)
longitude := geodata.NormalizeLongitude(riseEndpoint.point.Longitude + math.Remainder(setEndpoint.point.Longitude-riseEndpoint.point.Longitude, 360)/2)
latitude := (riseEndpoint.point.Latitude + setEndpoint.point.Latitude) / 2
junction, ok = refineOccultationRiseSetDirectionJunction(
(occultationTimeToTT(riseEndpoint.point.Time)+occultationTimeToTT(setEndpoint.point.Time))/2,
@@ -919,7 +939,7 @@ func refineOccultationRiseSetPhaseJunction(
return OccultationPathPoint{}, false
}
occultationRiseSetLimitJunctionDelta(&delta)
coordinates[0] = normalizeLongitude(coordinates[0] + delta[0])
coordinates[0] = geodata.NormalizeLongitude(coordinates[0] + delta[0])
coordinates[1] += delta[1]
coordinates[2] += delta[2]
if coordinates[1] <= -89.999999 || coordinates[1] >= 89.999999 {
@@ -1031,7 +1051,7 @@ func refineOccultationRiseSetDirectionJunction(
return OccultationPathPoint{}, false
}
occultationRiseSetLimitJunctionDelta(&delta)
coordinates[0] = normalizeLongitude(coordinates[0] + delta[0])
coordinates[0] = geodata.NormalizeLongitude(coordinates[0] + delta[0])
coordinates[1] += delta[1]
coordinates[2] += delta[2]
if coordinates[1] <= -89.999999 || coordinates[1] >= 89.999999 {
+4 -2
View File
@@ -3,13 +3,15 @@ package basic
import (
"testing"
"time"
"b612.me/astro/internal/geodata"
)
func TestOccultationRiseSetNewRootKeepsExistingBranch(t *testing.T) {
start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
for _, offset := range []float64{0, 160, -200} {
point := func(lon float64, at time.Time) OccultationPathPoint {
return OccultationPathPoint{Time: at, Longitude: normalizeLongitude(lon + offset), Latitude: 58}
return OccultationPathPoint{Time: at, Longitude: geodata.NormalizeLongitude(lon + offset), Latitude: 58}
}
tracks := []*occultationRiseSetTrack{{segments: [][]OccultationPathPoint{{point(20, start)}}}}
tracks = appendOccultationRiseSetSamples(tracks, []OccultationPathPoint{
@@ -18,7 +20,7 @@ func TestOccultationRiseSetNewRootKeepsExistingBranch(t *testing.T) {
if len(tracks) != 2 || len(tracks[0].segments[0]) != 2 || len(tracks[1].segments[0]) != 1 {
t.Fatalf("offset=%g: new root did not get its own track", offset)
}
if got := tracks[0].segments[0][1].Longitude; got != normalizeLongitude(19+offset) {
if got := tracks[0].segments[0][1].Longitude; got != geodata.NormalizeLongitude(19+offset) {
t.Errorf("offset=%g: existing track jumped to longitude %g", offset, got)
}
}
+144 -128
View File
@@ -4,6 +4,8 @@ import (
"math"
"sort"
"time"
"b612.me/astro/internal/iau2006"
)
const (
@@ -18,15 +20,9 @@ const (
starOccultationMaxContactSteps = 10000
)
// FindStarOccultations 搜索单颗点源恒星的月掩星。
//
// 输入坐标会从历元传播并转换到当日视坐标系;若提供视差,还会修正观测者的恒星视差。
// 搜索不会加载内嵌 9100 星表;需要全星表搜索时,调用者必须显式加载并选择恒星。经度东为正、纬度北为正,单位为度;高度为平均海平面以上米数。
// FindStarOccultations searches for lunar occultations of one point-source star.
// The input coordinate is propagated from its epoch, converted to the apparent frame of date, and corrected for the observer's stellar parallax when one is supplied.
// The search does not load the embedded 9100-star catalog; callers that need a catalog-wide search must load and select stars explicitly. Longitude is east-positive in degrees, latitude is north-positive in degrees, and height is the observer elevation above mean sea level in meters.
func FindStarOccultations(start, end time.Time, star StarCoordinate, longitude, latitude, height float64,
options OccultationSearchOptions) ([]StarOccultationInfo, error) {
// FindStarOccultationsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func FindStarOccultationsWithInstance(start, end time.Time, star StarCoordinate, longitude, latitude, height float64,
options OccultationSearchOptions, inst *Instance) ([]StarOccultationInfo, error) {
if err := validateOccultationTimeRange(start, end); err != nil {
return nil, err
}
@@ -45,8 +41,8 @@ func FindStarOccultations(start, end time.Time, star StarCoordinate, longitude,
endTT := occultationTimeToTT(end)
resultLocation := start.Location()
results := make([]StarOccultationInfo, 0)
for _, greatestTT := range starOccultationCandidateGreatestTimes(startTT, endTT, starOccultationCoarseStepDays(options), star, observer, options.SafetyMarginArcsec) {
info, ok := starOccultationInfoAtGreatest(greatestTT, star, observer, options.SafetyMarginArcsec, resultLocation)
for _, greatestTT := range starOccultationCandidateGreatestTimes(startTT, endTT, starOccultationCoarseStepDays(options), star, observer, options.SafetyMarginArcsec, inst) {
info, ok := starOccultationInfoWithLimb(greatestTT, star, observer, options.SafetyMarginArcsec, resultLocation, options.LunarLimb, inst)
if !ok {
continue
}
@@ -62,14 +58,8 @@ func FindStarOccultations(start, end time.Time, star StarCoordinate, longitude,
return results, nil
}
// FindBestStarOccultations 返回窗口内每次恒星月掩在地球上的全球几何掩甚点。
// 返回的 StarOccultationInfo.Observer 是海平面大地测量位置,由月掩几何选择,不使用地平线或可见性评分。
//
// 地心数据只用于月周期搜索初值;最终点是与 FindStarOccultationPaths 一致的标准全球掩甚路径点,然后在该处重新进行站心接触和可见性计算。查询端点 10 ms 内的掩甚时刻也会包含,与数值根精度一致。
// FindBestStarOccultations returns the global geometric greatest point on Earth for each stellar occultation in the window.
// The returned StarOccultationInfo.Observer is the geodetic location at sea level; it is selected from the occultation geometry, without a horizon or visibility score.
// Geocentric data only seeds each lunar-month search. The final point is the canonical global greatest-path point, matching FindStarOccultationPaths; event contacts and visibility are then recomputed topocentrically there. A greatest instant within 10 ms of either query endpoint is included, matching the numerical root precision.
func FindBestStarOccultations(start, end time.Time, star StarCoordinate, options OccultationSearchOptions) ([]StarOccultationInfo, error) {
// FindBestStarOccultationsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func FindBestStarOccultationsWithInstance(start, end time.Time, star StarCoordinate, options OccultationSearchOptions, inst *Instance) ([]StarOccultationInfo, error) {
if err := validateOccultationTimeRange(start, end); err != nil {
return nil, err
}
@@ -88,12 +78,12 @@ func FindBestStarOccultations(start, end time.Time, star StarCoordinate, options
candidateEndTT := endTT + occultationPathSearchSpanDays
resultLocation := start.Location()
results := make([]StarOccultationInfo, 0)
for _, seedTT := range starOccultationGeocentricCandidateGreatestTimes(candidateStartTT, candidateEndTT, starOccultationCoarseStepDays(options), star, options.SafetyMarginArcsec) {
greatestTT, observer, _, observerOK := starOccultationBestObserver(seedTT, selectionStartTT, selectionEndTT, star)
for _, seedTT := range starOccultationGeocentricCandidateGreatestTimes(candidateStartTT, candidateEndTT, starOccultationCoarseStepDays(options), star, options.SafetyMarginArcsec, inst) {
greatestTT, observer, _, observerOK := starOccultationBestObserver(seedTT, selectionStartTT, selectionEndTT, star, inst)
if !observerOK {
continue
}
info, ok := starOccultationInfoAtGreatest(greatestTT, star, observer, options.SafetyMarginArcsec, resultLocation)
info, ok := starOccultationInfoWithLimb(greatestTT, star, observer, options.SafetyMarginArcsec, resultLocation, options.LunarLimb, inst)
if !ok {
continue
}
@@ -108,40 +98,44 @@ func FindBestStarOccultations(start, end time.Time, star StarCoordinate, options
return results, nil
}
func starOccultationCandidateGreatestTimes(startTT, endTT, step float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64) []float64 {
func starOccultationCandidateGreatestTimes(startTT, endTT, step float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64, instances ...*Instance) []float64 {
inst := instanceValue(instances)
results := make([]float64, 0)
for cycleStart := startTT; cycleStart < endTT; cycleStart += starOccultationSiderealMonthDays {
cycleEnd := math.Min(cycleStart+starOccultationSiderealMonthDays, endTT)
scanStart := math.Max(startTT-step, cycleStart-step)
scanEnd := math.Min(endTT+step, cycleEnd+step)
if !starOccultationLatitudeEnvelopePass(scanStart, scanEnd, star, &observer, safetyMarginArcsec) {
if !starOccultationLatitudeEnvelopePass(scanStart, scanEnd, star, &observer, safetyMarginArcsec, inst) {
continue
}
results = append(results, starOccultationScanCandidates(
scanStart, scanEnd, step,
func(tt float64) float64 { return starMoonSeparationArcsec(tt, star, observer) },
func(tt float64) float64 { return starMoonSeparationArcsec(tt, star, observer, inst) },
func(tt float64) bool {
return starOccultationLatitudePass(tt, star, observer, safetyMarginArcsec)
return starOccultationLatitudePass(tt, star, observer, safetyMarginArcsec, inst)
},
)...)
}
return uniqueOccultationCandidateTimes(results, startTT, endTT)
}
func starOccultationGeocentricCandidateGreatestTimes(startTT, endTT, step float64, star StarCoordinate, safetyMarginArcsec float64) []float64 {
func starOccultationGeocentricCandidateGreatestTimes(startTT, endTT, step float64, star StarCoordinate, safetyMarginArcsec float64, instances ...*Instance) []float64 {
inst := instanceValue(instances)
results := make([]float64, 0)
for cycleStart := startTT; cycleStart < endTT; cycleStart += starOccultationSiderealMonthDays {
cycleEnd := math.Min(cycleStart+starOccultationSiderealMonthDays, endTT)
scanStart := math.Max(startTT-step, cycleStart-step)
scanEnd := math.Min(endTT+step, cycleEnd+step)
if !starOccultationLatitudeEnvelopePass(scanStart, scanEnd, star, nil, safetyMarginArcsec) {
if !starOccultationLatitudeEnvelopePass(scanStart, scanEnd, star, nil, safetyMarginArcsec, inst) {
continue
}
results = append(results, starOccultationScanCandidates(
scanStart, scanEnd, step,
func(tt float64) float64 { return starOccultationGeocentricSeparationArcsec(tt, star) },
func(tt float64) float64 { return starOccultationGeocentricSeparationArcsec(tt, star, inst) },
func(tt float64) bool {
return starOccultationLatitudePassGeocentric(tt, star, safetyMarginArcsec)
return starOccultationLatitudePassGeocentric(tt, star, safetyMarginArcsec, inst)
},
)...)
}
@@ -216,23 +210,25 @@ func uniqueOccultationCandidateTimes(times []float64, startTT, endTT float64) []
return unique
}
func starOccultationLatitudeEnvelopePass(startTT, endTT float64, star StarCoordinate, observer *Observer, safetyMarginArcsec float64) bool {
func starOccultationLatitudeEnvelopePass(startTT, endTT float64, star StarCoordinate, observer *Observer, safetyMarginArcsec float64, instances ...*Instance) bool {
inst := instanceValue(instances)
minimumLatitude := math.Inf(1)
maximumLatitude := math.Inf(-1)
maximumMoonRadiusDeg := 0.0
for _, tt := range []float64{startTT, (startTT + endTT) / 2, endTT} {
var ra, dec float64
if observer == nil {
ra, dec = starApparentRaDecGeocentric(tt, star)
ra, dec = starApparentRaDecGeocentric(tt, star, inst)
} else {
ra, dec = starApparentRaDec(tt, star, *observer)
ra, dec = starApparentRaDec(tt, star, *observer, inst)
}
_, latitude := RaDecToLoBo(tt, ra, dec)
minimumLatitude = math.Min(minimumLatitude, latitude)
maximumLatitude = math.Max(maximumLatitude, latitude)
moonRadius := MoonSemidiameter(tt)
moonRadius := MoonSemidiameterWithInstance(tt, inst)
if observer != nil {
moonRadius = moonTopocentricSemidiameterN(tt, *observer, -1)
moonRadius = moonTopocentricSemidiameterN(tt, *observer, -1, inst)
}
maximumMoonRadiusDeg = math.Max(maximumMoonRadiusDeg, moonRadius/3600)
}
@@ -271,27 +267,31 @@ func starOccultationMinimizeValue(left, right float64, value func(float64) float
return (left + right) / 2
}
func starOccultationGeocentricSeparationArcsec(tt float64, star StarCoordinate) float64 {
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1)
starRA, starDec, _ := starApparentRaDecDistanceGeocentric(tt, star)
func starOccultationGeocentricSeparationArcsec(tt float64, star StarCoordinate, instances ...*Instance) float64 {
inst := instanceValue(instances)
moonRA, moonDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
starRA, starDec, _ := starApparentRaDecDistanceGeocentric(tt, star, inst)
return angularSeparationDegrees(moonRA, moonDec, starRA, starDec) * 3600
}
func starOccultationBestObserver(seedTT, startTT, endTT float64, star StarCoordinate) (float64, Observer, float64, bool) {
func starOccultationBestObserver(seedTT, startTT, endTT float64, star StarCoordinate, instances ...*Instance) (float64, Observer, float64, bool) {
inst := instanceValue(instances)
searchStart := seedTT - occultationPathSearchSpanDays
searchEnd := seedTT + occultationPathSearchSpanDays
outerStart, outerEnd, ok := starOccultationPathWindow(seedTT, searchStart, searchEnd, star, false)
outerStart, outerEnd, ok := starOccultationPathWindow(seedTT, searchStart, searchEnd, star, false, inst)
if !ok {
return 0, Observer{}, 0, false
}
greatestTT := starOccultationPathGreatest(seedTT, outerStart, outerEnd, star)
greatestTT := starOccultationPathGreatest(seedTT, outerStart, outerEnd, star, inst)
if greatestTT < startTT || greatestTT > endTT {
return 0, Observer{}, 0, false
}
point, pointOK := starOccultationPathCenterPoint(greatestTT, star, time.UTC)
point, pointOK := starOccultationPathCenterPoint(greatestTT, star, time.UTC, inst)
if !pointOK {
frameAt := func(tt float64) (occultationPathFrame, bool) {
return starOccultationPathFrameAt(tt, star)
return starOccultationPathFrameAt(tt, star, inst)
}
// 同行星最佳事件搜索:非中心路径的最佳站心取离影轴最近的椭球点,
// 外接触切点位于掩带边缘,只作最后回退。
@@ -300,15 +300,15 @@ func starOccultationBestObserver(seedTT, startTT, endTT float64, star StarCoordi
// the outer contact tangent on the band edge stays the last resort.
point, pointOK = occultationPathTrackPointForFrame(greatestTT, frameAt, time.UTC)
if !pointOK {
point, pointOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, time.UTC)
point, pointOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, time.UTC, inst)
}
}
if !pointOK {
return 0, Observer{}, 0, false
}
observer := Observer{Longitude: point.Longitude, Latitude: point.Latitude}
position := starMoonPositionAt(greatestTT, star, observer)
moonRadius := moonTopocentricSemidiameterN(greatestTT, observer, -1)
position := starMoonPositionAt(greatestTT, star, observer, inst)
moonRadius := moonTopocentricSemidiameterN(greatestTT, observer, -1, inst)
if !position.valid || !finite(moonRadius) {
return 0, Observer{}, 0, false
}
@@ -316,24 +316,43 @@ func starOccultationBestObserver(seedTT, startTT, endTT float64, star StarCoordi
return greatestTT, observer, metric, finite(metric)
}
func normalizeLongitude180(longitude float64) float64 {
longitude = math.Mod(longitude+180, 360)
if longitude < 0 {
longitude += 360
func starOccultationInfoAtGreatest(greatestTT float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64, location *time.Location, instances ...*Instance) (StarOccultationInfo, bool) {
inst := instanceValue(instances)
info, ok := starOccultationGreatestInfo(greatestTT, star, observer, safetyMarginArcsec, location, inst)
if !ok || info.MinimumSeparationArcsec > info.MoonSemidiameterArcsec {
return StarOccultationInfo{}, false
}
return longitude - 180
minimumResidual := info.MinimumSeparationArcsec - info.MoonSemidiameterArcsec
if math.Abs(minimumResidual) <= starOccultationGrazingTolerance {
info.Type = OccultationGrazing
info.Immersion = info.Greatest
info.Emersion = info.Greatest
info.ContactsComplete = true
return info, true
}
immersionTT, immersionOK := starOccultationContact(greatestTT, greatestTT-starOccultationContactSpanDays, -1, star, observer, inst)
emersionTT, emersionOK := starOccultationContact(greatestTT, greatestTT+starOccultationContactSpanDays, 1, star, observer, inst)
if !immersionOK || !emersionOK {
return StarOccultationInfo{}, false
}
info.Immersion = occultationTTToLocation(immersionTT, location)
info.Emersion = occultationTTToLocation(emersionTT, location)
info.ContactsComplete = true
return info, true
}
func starOccultationInfoAtGreatest(greatestTT float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64, location *time.Location) (StarOccultationInfo, bool) {
if !starOccultationLatitudePass(greatestTT, star, observer, safetyMarginArcsec) {
// starOccultationGreatestInfo 只填掩甚几何,不判断圆月面是否成掩、不求接触。
func starOccultationGreatestInfo(greatestTT float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64, location *time.Location, inst *Instance) (StarOccultationInfo, bool) {
if !starOccultationLatitudePass(greatestTT, star, observer, safetyMarginArcsec, inst) {
return StarOccultationInfo{}, false
}
minimumSeparation := starMoonSeparationArcsec(greatestTT, star, observer)
moonRadius := moonTopocentricSemidiameterN(greatestTT, observer, -1)
if !finite(minimumSeparation) || !finite(moonRadius) || minimumSeparation > moonRadius {
minimumSeparation := starMoonSeparationArcsec(greatestTT, star, observer, inst)
moonRadius := moonTopocentricSemidiameterN(greatestTT, observer, -1, inst)
if !finite(minimumSeparation) || !finite(moonRadius) {
return StarOccultationInfo{}, false
}
greatestPosition := starMoonPositionAt(greatestTT, star, observer)
greatestPosition := starMoonPositionAt(greatestTT, star, observer, inst)
if !greatestPosition.valid {
return StarOccultationInfo{}, false
}
@@ -349,23 +368,6 @@ func starOccultationInfoAtGreatest(greatestTT float64, star StarCoordinate, obse
MoonAzimuthAtGreatest: occultationAzimuth(greatestTT, observer, greatestPosition.moonRA, greatestPosition.moonDec),
}
info.VisibleAtGreatest = info.MoonAltitudeAtGreatest >= 0
minimumResidual := minimumSeparation - moonRadius
if math.Abs(minimumResidual) <= starOccultationGrazingTolerance {
info.Type = OccultationGrazing
info.Immersion = info.Greatest
info.Emersion = info.Greatest
info.ContactsComplete = true
return info, true
}
immersionTT, immersionOK := starOccultationContact(greatestTT, greatestTT-starOccultationContactSpanDays, -1, star, observer)
emersionTT, emersionOK := starOccultationContact(greatestTT, greatestTT+starOccultationContactSpanDays, 1, star, observer)
if !immersionOK || !emersionOK {
return StarOccultationInfo{}, false
}
info.Immersion = occultationTTToLocation(immersionTT, location)
info.Emersion = occultationTTToLocation(emersionTT, location)
info.ContactsComplete = true
return info, true
}
@@ -375,9 +377,11 @@ type starMoonPosition struct {
valid bool
}
func starMoonPositionAt(tt float64, star StarCoordinate, observer Observer) starMoonPosition {
moonRA, moonDec := moonTopocentricApparentRaDec(tt, observer, -1)
starRA, starDec := starApparentRaDec(tt, star, observer)
func starMoonPositionAt(tt float64, star StarCoordinate, observer Observer, instances ...*Instance) starMoonPosition {
inst := instanceValue(instances)
moonRA, moonDec := moonTopocentricApparentRaDec(tt, observer, -1, inst)
starRA, starDec := starApparentRaDec(tt, star, observer, inst)
return starMoonPosition{
moonRA: moonRA,
moonDec: moonDec,
@@ -387,16 +391,20 @@ func starMoonPositionAt(tt float64, star StarCoordinate, observer Observer) star
}
}
func moonTopocentricApparentRaDec(tt float64, observer Observer, n int) (float64, float64) {
ra, dec := HMoonGeocentricApparentRaDecN(tt, n)
func moonTopocentricApparentRaDec(tt float64, observer Observer, n int, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
ra, dec := HMoonGeocentricApparentRaDecNWithInstance(tt, n, inst)
ut := TT2UTC(tt)
distanceAU := HMoonAwayN(tt, n) / 149597870.7
distanceAU := HMoonAwayNWithInstance(tt, n, inst) / 149597870.7
ra, dec = TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, ut, distanceAU, observer.Height)
return normalizeRA(ra), dec
}
func starApparentRaDec(tt float64, star StarCoordinate, observer Observer) (float64, float64) {
ra, dec, distanceAU := starApparentRaDecDistanceGeocentric(tt, star)
func starApparentRaDec(tt float64, star StarCoordinate, observer Observer, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
ra, dec, distanceAU := starApparentRaDecDistanceGeocentric(tt, star, inst)
if distanceAU > 0 {
ra, dec = TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, TT2UTC(tt), distanceAU, observer.Height)
ra = normalizeRA(ra)
@@ -404,13 +412,17 @@ func starApparentRaDec(tt float64, star StarCoordinate, observer Observer) (floa
return ra, dec
}
func starApparentRaDecGeocentric(tt float64, star StarCoordinate) (float64, float64) {
ra, dec, _ := starApparentRaDecDistanceGeocentric(tt, star)
func starApparentRaDecGeocentric(tt float64, star StarCoordinate, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
ra, dec, _ := starApparentRaDecDistanceGeocentric(tt, star, inst)
return ra, dec
}
// starApparentRaDecDistanceGeocentric 同时给出视位置与推进后的距离(天文单位,0 表示距离未知)。
func starApparentRaDecDistanceGeocentric(tt float64, star StarCoordinate) (float64, float64, float64) {
func starApparentRaDecDistanceGeocentric(tt float64, star StarCoordinate, instances ...*Instance) (float64, float64, float64) {
inst := instanceValue(instances)
epochJD := occultationTimeToTT(star.Epoch)
parallaxMas := star.parallaxMas()
ra := star.RA
@@ -419,13 +431,13 @@ func starApparentRaDecDistanceGeocentric(tt float64, star StarCoordinate) (float
if star.Frame == CoordinateFrameICRS {
ra, dec = starICRSToMeanJ2000RaDec(ra, dec)
} else if star.Frame == CoordinateFrameApparentOfDate {
ra, dec = starApparentToMeanRaDec(epochJD, ra, dec, parallaxMas)
ra, dec = starApparentToMeanRaDec(epochJD, ra, dec, parallaxMas, inst)
precessionEpoch = epochJD
}
epochDistanceAU := starEpochDistanceAU(parallaxMas)
ra, dec, distanceAU := starProperMotionRaDec(tt, star, ra, dec, epochDistanceAU)
ra, dec = Precess(ra, dec, precessionEpoch, tt)
ra, dec = starMeanToApparentRaDec(tt, ra, dec, parallaxMasAtDistance(parallaxMas, distanceAU))
ra, dec = starMeanToApparentRaDec(tt, ra, dec, parallaxMasAtDistance(parallaxMas, distanceAU), inst)
return ra, dec, distanceAU
}
@@ -493,47 +505,39 @@ func starProperMotionRaDec3D(ra, dec, years, epochDistanceAU float64, star StarC
}
func starICRSToMeanJ2000RaDec(ra, dec float64) (float64, float64) {
// IAU SOFA 框架偏差矩阵,将 GCRS/ICRS 向量转换为 J2000.0 平均赤道和春分点。
// IAU SOFA frame-bias matrix, transforming a GCRS/ICRS vector to the mean equator and equinox of J2000.0.
const (
b00 = 0.9999999999999942
b01 = -0.7078279744199197e-7
b02 = 0.8056217146976134e-7
b10 = 0.7078279477857337e-7
b11 = 0.9999999999999969
b12 = 0.3306041454222148e-7
b20 = -0.8056217380986972e-7
b21 = -0.3306040883980553e-7
b22 = 0.9999999999999962
)
b := iau2006.FrameBias()
raRad := ra * math.Pi / 180
decRad := dec * math.Pi / 180
x := math.Cos(decRad) * math.Cos(raRad)
y := math.Cos(decRad) * math.Sin(raRad)
z := math.Sin(decRad)
biasedX := b00*x + b01*y + b02*z
biasedY := b10*x + b11*y + b12*z
biasedZ := b20*x + b21*y + b22*z
biasedX := b[0][0]*x + b[0][1]*y + b[0][2]*z
biasedY := b[1][0]*x + b[1][1]*y + b[1][2]*z
biasedZ := b[2][0]*x + b[2][1]*y + b[2][2]*z
return normalizeRA(math.Atan2(biasedY, biasedX) * 180 / math.Pi),
math.Atan2(biasedZ, math.Hypot(biasedX, biasedY)) * 180 / math.Pi
}
func starMeanToApparentRaDec(tt, ra, dec, parallaxMas float64) (float64, float64) {
func starMeanToApparentRaDec(tt, ra, dec, parallaxMas float64, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
longitude, latitude := starMeanEquatorialToEcliptic(tt, ra, dec)
if parallaxMas > 0 {
longitude, latitude = starAnnualParallaxEcliptic(tt, longitude, latitude, parallaxMas)
longitude, latitude = starAnnualParallaxEcliptic(tt, longitude, latitude, parallaxMas, inst)
}
meanLongitude, meanLatitude := longitude, latitude
longitude = normalizeRA(meanLongitude + GXCLo(meanLongitude, meanLatitude, tt)/3600 + Nutation2000Bi(tt))
latitude = meanLatitude + GXCBo(meanLongitude, meanLatitude, tt)/3600
longitude = normalizeRA(meanLongitude + GXCLoWithInstance(meanLongitude, meanLatitude, tt, inst)/3600 + Nutation2000Bi(tt))
latitude = meanLatitude + GXCBoWithInstance(meanLongitude, meanLatitude, tt, inst)/3600
ra, dec = LoBoToRaDec(tt, longitude, latitude)
return normalizeRA(ra), dec
}
func starApparentToMeanRaDec(tt, apparentRA, apparentDec, parallaxMas float64) (float64, float64) {
func starApparentToMeanRaDec(tt, apparentRA, apparentDec, parallaxMas float64, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
meanRA, meanDec := apparentRA, apparentDec
for i := 0; i < 8; i++ {
computedRA, computedDec := starMeanToApparentRaDec(tt, meanRA, meanDec, parallaxMas)
computedRA, computedDec := starMeanToApparentRaDec(tt, meanRA, meanDec, parallaxMas, inst)
meanRA = normalizeRA(meanRA - signedAngleDifference(computedRA, apparentRA))
meanDec -= computedDec - apparentDec
}
@@ -554,7 +558,9 @@ func starMeanEquatorialToEcliptic(tt, ra, dec float64) (float64, float64) {
return normalizeRA(longitude), latitude
}
func starAnnualParallaxEcliptic(tt, longitude, latitude, parallaxMas float64) (float64, float64) {
func starAnnualParallaxEcliptic(tt, longitude, latitude, parallaxMas float64, instances ...*Instance) (float64, float64) {
inst := instanceValue(instances)
distanceAU := 206264806.247 / parallaxMas
longitudeRad := longitude * math.Pi / 180
latitudeRad := latitude * math.Pi / 180
@@ -563,8 +569,8 @@ func starAnnualParallaxEcliptic(tt, longitude, latitude, parallaxMas float64) (f
starY := distanceAU * cosLatitude * math.Sin(longitudeRad)
starZ := distanceAU * math.Sin(latitudeRad)
earthLongitude := normalizeRA(HSunTrueLoN(tt, -1)+180) * math.Pi / 180
earthDistance := EarthAwayN(tt, -1)
earthLongitude := normalizeRA(HSunTrueLoNWithInstance(tt, -1, inst)+180) * math.Pi / 180
earthDistance := EarthAwayNWithInstance(tt, -1, inst)
starX -= earthDistance * math.Cos(earthLongitude)
starY -= earthDistance * math.Sin(earthLongitude)
@@ -580,33 +586,41 @@ func starOccultationLongitudeDistance(tt float64, star StarCoordinate, observer
return math.Abs(signedAngleDifference(moonLongitude, starLongitude))
}
func starMoonSeparationArcsec(tt float64, star StarCoordinate, observer Observer) float64 {
position := starMoonPositionAt(tt, star, observer)
func starMoonSeparationArcsec(tt float64, star StarCoordinate, observer Observer, instances ...*Instance) float64 {
inst := instanceValue(instances)
position := starMoonPositionAt(tt, star, observer, inst)
if !position.valid {
return math.Inf(1)
}
return angularSeparationDegrees(position.moonRA, position.moonDec, position.starRA, position.starDec) * 3600
}
func starOccultationLatitudePass(tt float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64) bool {
starRA, starDec := starApparentRaDec(tt, star, observer)
func starOccultationLatitudePass(tt float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64, instances ...*Instance) bool {
inst := instanceValue(instances)
starRA, starDec := starApparentRaDec(tt, star, observer, inst)
_, starLatitude := RaDecToLoBo(tt, starRA, starDec)
moonLatitude := HMoonTrueBoN(tt, 8)
moonRadius := moonTopocentricSemidiameterN(tt, observer, -1)
moonLatitude := HMoonTrueBoNWithInstance(tt, 8, inst)
moonRadius := moonTopocentricSemidiameterN(tt, observer, -1, inst)
limit := moonRadius + starOccultationLatitudeMarginAS + safetyMarginArcsec
return math.Abs(starLatitude-moonLatitude)*3600 <= limit
}
func starOccultationLatitudePassGeocentric(tt float64, star StarCoordinate, safetyMarginArcsec float64) bool {
starRA, starDec := starApparentRaDecGeocentric(tt, star)
func starOccultationLatitudePassGeocentric(tt float64, star StarCoordinate, safetyMarginArcsec float64, instances ...*Instance) bool {
inst := instanceValue(instances)
starRA, starDec := starApparentRaDecGeocentric(tt, star, inst)
_, starLatitude := RaDecToLoBo(tt, starRA, starDec)
moonLatitude := HMoonTrueBoN(tt, 8)
limit := MoonSemidiameter(tt) + starOccultationLatitudeMarginAS + safetyMarginArcsec
moonLatitude := HMoonTrueBoNWithInstance(tt, 8, inst)
limit := MoonSemidiameterWithInstance(tt, inst) + starOccultationLatitudeMarginAS + safetyMarginArcsec
return math.Abs(starLatitude-moonLatitude)*3600 <= limit
}
func starOccultationContact(greatestTT, boundaryTT float64, direction int, star StarCoordinate, observer Observer) (float64, bool) {
valueAtGreatest := starMoonSeparationArcsec(greatestTT, star, observer) - moonTopocentricSemidiameterN(greatestTT, observer, -1)
func starOccultationContact(greatestTT, boundaryTT float64, direction int, star StarCoordinate, observer Observer, instances ...*Instance) (float64, bool) {
inst := instanceValue(instances)
valueAtGreatest := starMoonSeparationArcsec(greatestTT, star, observer, inst) - moonTopocentricSemidiameterN(greatestTT, observer, -1, inst)
if !finite(valueAtGreatest) || valueAtGreatest > 0 {
return math.NaN(), false
}
@@ -621,9 +635,9 @@ func starOccultationContact(greatestTT, boundaryTT float64, direction int, star
if direction > 0 && nextTT > boundaryTT {
nextTT = boundaryTT
}
nextValue := starMoonSeparationArcsec(nextTT, star, observer) - moonTopocentricSemidiameterN(nextTT, observer, -1)
nextValue := starMoonSeparationArcsec(nextTT, star, observer, inst) - moonTopocentricSemidiameterN(nextTT, observer, -1, inst)
if finite(nextValue) && nextValue >= 0 {
return starOccultationRoot(currentTT, nextTT, currentValue, nextValue, star, observer)
return starOccultationRoot(currentTT, nextTT, currentValue, nextValue, star, observer, inst)
}
if nextTT == boundaryTT {
return math.NaN(), false
@@ -634,7 +648,9 @@ func starOccultationContact(greatestTT, boundaryTT float64, direction int, star
return math.NaN(), false
}
func starOccultationRoot(leftTT, rightTT, leftValue, rightValue float64, star StarCoordinate, observer Observer) (float64, bool) {
func starOccultationRoot(leftTT, rightTT, leftValue, rightValue float64, star StarCoordinate, observer Observer, instances ...*Instance) (float64, bool) {
inst := instanceValue(instances)
if !finite(leftValue) || !finite(rightValue) || leftValue*rightValue > 0 {
return math.NaN(), false
}
@@ -646,7 +662,7 @@ func starOccultationRoot(leftTT, rightTT, leftValue, rightValue float64, star St
}
for i := 0; i < 64 && math.Abs(rightTT-leftTT) > starOccultationRootToleranceDays; i++ {
midTT := (leftTT + rightTT) / 2
midValue := starMoonSeparationArcsec(midTT, star, observer) - moonTopocentricSemidiameterN(midTT, observer, -1)
midValue := starMoonSeparationArcsec(midTT, star, observer, inst) - moonTopocentricSemidiameterN(midTT, observer, -1, inst)
if !finite(midValue) {
return math.NaN(), false
}
+15 -14
View File
@@ -67,13 +67,11 @@ type starOccultationDiagramTime struct {
labels []string
}
// StarOccultationDiagram 为已求解的固定地点恒星月掩计算以月心为原点的切平面轨迹。事件数据无效或不完整时,结果不含帧。
// StarOccultationDiagram computes a Moon-centered tangent-plane track for an already solved fixed-site stellar occultation. Invalid or incomplete event data produces a result without frames.
func StarOccultationDiagram(
// StarOccultationDiagramWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func StarOccultationDiagramWithInstance(
info StarOccultationInfo,
star StarCoordinate,
options StarOccultationDiagramOptions,
) StarOccultationDiagramResult {
options StarOccultationDiagramOptions, inst *Instance) StarOccultationDiagramResult {
options = normalizeStarOccultationDiagramOptions(options)
result := StarOccultationDiagramResult{Occultation: info, StepDays: options.StepDays}
if star.Validate() != nil || info.Observer.Validate() != nil ||
@@ -86,9 +84,9 @@ func StarOccultationDiagram(
startTT := occultationTimeToTT(info.Immersion)
greatestTT := occultationTimeToTT(info.Greatest)
endTT := occultationTimeToTT(info.Emersion)
immersionFrame, immersionOK := starOccultationDiagramFrameAt(startTT, star, info.Observer)
greatestFrame, greatestOK := starOccultationDiagramFrameAt(greatestTT, star, info.Observer)
emersionFrame, emersionOK := starOccultationDiagramFrameAt(endTT, star, info.Observer)
immersionFrame, immersionOK := starOccultationDiagramFrameAt(startTT, star, info.Observer, inst)
greatestFrame, greatestOK := starOccultationDiagramFrameAt(greatestTT, star, info.Observer, inst)
emersionFrame, emersionOK := starOccultationDiagramFrameAt(endTT, star, info.Observer, inst)
if !immersionOK || !greatestOK || !emersionOK ||
!starOccultationDiagramMatchesInfo(info, immersionFrame, greatestFrame, emersionFrame) {
return result
@@ -97,7 +95,7 @@ func StarOccultationDiagram(
result.StepDays = stepDays
result.Frames = make([]StarOccultationDiagramFrame, 0, len(times))
for _, item := range times {
frame, ok := starOccultationDiagramFrameAt(item.jde, star, info.Observer)
frame, ok := starOccultationDiagramFrameAt(item.jde, star, info.Observer, inst)
if !ok {
return StarOccultationDiagramResult{Occultation: info, StepDays: stepDays}
}
@@ -116,8 +114,9 @@ func starOccultationDiagramMatchesInfo(
!finite(info.PositionAngleDeg) {
return false
}
if math.Abs(immersion.SeparationArcsec-immersion.MoonRadiusArcsec) > starOccultationDiagramGeometryArcsec ||
math.Abs(emersion.SeparationArcsec-emersion.MoonRadiusArcsec) > starOccultationDiagramGeometryArcsec {
// 已按月缘剖面收敛的接触不在圆月面上,圆月面接触判据只适用于未修正的结果。
if !info.LunarLimbApplied && (math.Abs(immersion.SeparationArcsec-immersion.MoonRadiusArcsec) > starOccultationDiagramGeometryArcsec ||
math.Abs(emersion.SeparationArcsec-emersion.MoonRadiusArcsec) > starOccultationDiagramGeometryArcsec) {
return false
}
return math.Abs(greatest.SeparationArcsec-info.MinimumSeparationArcsec) <= starOccultationDiagramGeometryArcsec &&
@@ -221,9 +220,11 @@ func starOccultationDiagramLabelPriority(labels []string) int {
}
}
func starOccultationDiagramFrameAt(tt float64, star StarCoordinate, observer Observer) (StarOccultationDiagramFrame, bool) {
position := starMoonPositionAt(tt, star, observer)
moonRadius := moonTopocentricSemidiameterN(tt, observer, -1)
func starOccultationDiagramFrameAt(tt float64, star StarCoordinate, observer Observer, instances ...*Instance) (StarOccultationDiagramFrame, bool) {
inst := instanceValue(instances)
position := starMoonPositionAt(tt, star, observer, inst)
moonRadius := moonTopocentricSemidiameterN(tt, observer, -1, inst)
if !position.valid || !finite(moonRadius) || moonRadius <= 0 {
return StarOccultationDiagramFrame{}, false
}
+461
View File
@@ -0,0 +1,461 @@
package basic
import (
"fmt"
"math"
"sort"
"time"
)
const (
starOccultationLimbDefaultStep = 200 * time.Millisecond
starOccultationLimbMinimumStep = 10 * time.Millisecond
starOccultationLimbMaxSamples = 20000
starOccultationLimbMinSamples = 64
starOccultationLimbBoundStep = 60.0 / 86400.0
starOccultationLimbBoundSpan = 3.5 / 24.0
starOccultationLimbNodeDays = 10.0 / 1440.0
starOccultationLimbNodeSpan = 4.0 / 24.0
starOccultationLimbRootDays = 1e-3 / 86400.0
starOccultationLimbUnrangedPadKM = 1.0
)
// StarOccultationLimbProfileOptions 控制精密恒星月掩的星迹采样 / sampling of a precise stellar occultation.
type StarOccultationLimbProfileOptions struct {
// Step 是星迹采样步长;零值 200 ms,小于 10 ms 提升到 10 ms;每段至少 64 个样本、总数不超过 20000,越界时自动调整。短于步长的遮挡或复现可能漏检 / track sampling step.
Step time.Duration
}
// StarOccultationLimbSample 星迹上一个时刻的月缘几何,角量均为角秒、度 / limb geometry at one instant on the star track.
type StarOccultationLimbSample struct {
Time time.Time
// SeparationArcsec 恒星到站心月心的角距 / star–Moon-centre separation.
SeparationArcsec float64
// LimbRadiusArcsec 恒星方向上的月缘角半径 / limb radius toward the star.
LimbRadiusArcsec float64
// MeanRadiusArcsec 参考圆月面角半径 / circular reference radius.
MeanRadiusArcsec float64
// PositionAngleDeg 恒星相对月心的位置角,自天北向东 / position angle of the star.
PositionAngleDeg float64
// Hidden 恒星位于月缘以内 / star is behind the limb.
Hidden bool
}
// OccultationInterval 一段连续遮挡 / one continuous hidden interval.
type OccultationInterval struct {
Start time.Time
End time.Time
}
// StarOccultationLimbProfileResult 一次恒星月掩沿星迹的月缘剖面与全部遮挡区间 / limb profile and every hidden interval of a stellar occultation.
type StarOccultationLimbProfileResult struct {
// Occultation 按同一剖面求得的标准结果,掩始、掩终为首次遮挡与最后复现 / standard result on the same profile.
Occultation StarOccultationInfo
// Segments 月缘起伏带内的采样段,按时间排序;深掩时为掩入侧与复现侧两段,掠掩时通常为一段 / samples inside the limb relief band.
Segments [][]StarOccultationLimbSample
// Hidden 全部遮挡区间,按时间排序 / every hidden interval.
Hidden []OccultationInterval
// ReferenceRadiusKM 样本 MeanRadiusArcsec 对应的参考月球半径 / reference lunar radius behind MeanRadiusArcsec.
ReferenceRadiusKM float64
// ReliefMinKM / ReliefMaxKM 判定所用的月缘半径范围 / limb radius range used for gating.
ReliefMinKM float64
ReliefMaxKM float64
// Step 实际采用的采样步长 / effective step.
Step time.Duration
}
type starLimbState struct {
separation, limbRadius, meanRadius, positionAngle float64
distanceKM float64
}
// starLimbEphemeris 掩甚前后 ±4 h 的地心月球、恒星视位置与月球本体坐标架插值表;站心换算逐点精确计算。
type starLimbEphemeris struct {
bodies []localEphemerisVectorNode
axes []localEphemerisVectorNode
base lunarLimbFrame
hasDistance bool
}
func newStarLimbEphemeris(greatestTT float64, star StarCoordinate, limb LunarLimb, inst *Instance) (*starLimbEphemeris, bool) {
half := int(math.Round(starOccultationLimbNodeSpan / starOccultationLimbNodeDays))
ephemeris := &starLimbEphemeris{
bodies: make([]localEphemerisVectorNode, 2*half+1),
axes: make([]localEphemerisVectorNode, 2*half+1),
}
for index := range ephemeris.bodies {
tt := greatestTT + float64(index-half)*starOccultationLimbNodeDays
moonRA, moonDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
moon := lunarEclipseLimbVector(moonRA, moonDec, HMoonAwayNWithInstance(tt, -1, inst))
starRA, starDec, distanceAU := starApparentRaDecDistanceGeocentric(tt, star, inst)
if distanceAU <= 0 {
distanceAU = 1
} else {
ephemeris.hasDistance = true
}
frame := newLunarLimbFrame(limb, tt, moon, inst)
if !frame.valid || !finiteVector3(moon) {
return nil, false
}
if index == half {
ephemeris.base = frame
}
ephemeris.bodies[index] = localEphemerisVectorNode{tt: tt, first: moon, next: lunarEclipseLimbVector(starRA, starDec, distanceAU)}
ephemeris.axes[index] = localEphemerisVectorNode{tt: tt, first: frame.axisX, next: frame.axisZ}
}
return ephemeris, true
}
func (ephemeris *starLimbEphemeris) stateAt(tt float64, observer Observer) (starLimbState, bool) {
moon, starVector, ok := interpolateLocalEphemerisVectors(ephemeris.bodies, tt)
if !ok {
return starLimbState{}, false
}
axisX, axisZ, ok := interpolateLocalEphemerisVectors(ephemeris.axes, tt)
if !ok {
return starLimbState{}, false
}
geoDistance := math.Sqrt(lunarLimbDot(moon, moon))
geoRA := normalizeRA(math.Atan2(moon[1], moon[0]) / rad)
geoDec := math.Asin(clampUnit(moon[2]/geoDistance)) / rad
ut := TT2UTC(tt)
topoRA, topoDec := TopocentricRaDec(geoRA, geoDec, observer.Latitude, observer.Longitude, ut, geoDistance/149597870.7, observer.Height)
topoRA = normalizeRA(topoRA)
topoDistance := topocentricDistanceKM(geoRA, geoDec, geoDistance, observer, ut)
starDistance := math.Sqrt(lunarLimbDot(starVector, starVector))
starRA := normalizeRA(math.Atan2(starVector[1], starVector[0]) / rad)
starDec := math.Asin(clampUnit(starVector[2]/starDistance)) / rad
if ephemeris.hasDistance {
starRA, starDec = TopocentricRaDec(starRA, starDec, observer.Latitude, observer.Longitude, ut, starDistance, observer.Height)
starRA = normalizeRA(starRA)
}
if !finite(topoRA) || !finite(topoDec) || !finite(topoDistance) || topoDistance <= 0 || !finite(starRA) || !finite(starDec) {
return starLimbState{}, false
}
frame := ephemeris.base
if frame.axisX, ok = lunarLimbNormalize(axisX); !ok {
return starLimbState{}, false
}
if frame.axisZ, ok = lunarLimbNormalize(axisZ); !ok {
return starLimbState{}, false
}
// 两轴分别插值后不再严格正交,按 z 轴重新正交化 x 轴。
if frame.axisX, ok = lunarLimbNormalize(lunarLimbReject(frame.axisX, frame.axisZ)); !ok {
return starLimbState{}, false
}
frame.axisY = lunarLimbCross(frame.axisZ, frame.axisX)
radiusKM, ok := frame.radiusKM(lunarEclipseLimbVector(topoRA, topoDec, topoDistance), lunarEclipseLimbVector(starRA, starDec, 1), false)
if !ok {
return starLimbState{}, false
}
return starLimbState{
separation: angularSeparationDegrees(topoRA, topoDec, starRA, starDec) * 3600,
limbRadius: angularSemidiameterArcsec(radiusKM, topoDistance),
meanRadius: angularSemidiameterArcsec(moonEquatorialRadiusKM, topoDistance),
positionAngle: occultationPositionAngle(topoRA, topoDec, starRA, starDec),
distanceKM: topoDistance,
}, true
}
// starOccultationLimbRange 返回剖面半径范围;剖面不报告范围时在掩甚扫一圈并外扩 1 km。
func starOccultationLimbRange(tt float64, star StarCoordinate, observer Observer, limb LunarLimb, inst *Instance) (float64, float64, bool) {
geoRA, geoDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
geoDistance := HMoonAwayNWithInstance(tt, -1, inst)
frame := newLunarLimbFrame(limb, tt, lunarEclipseLimbVector(geoRA, geoDec, geoDistance), inst)
if !frame.valid {
return 0, 0, false
}
if frame.ranged {
return frame.minKM, frame.maxKM, true
}
topoRA, topoDec := moonTopocentricApparentRaDec(tt, observer, -1, inst)
starRA, starDec := starApparentRaDec(tt, star, observer, inst)
view, ok := frame.viewToward(lunarEclipseLimbVector(topoRA, topoDec, 1), lunarEclipseLimbVector(starRA, starDec, 1))
if !ok {
return 0, 0, false
}
low, high := math.Inf(1), math.Inf(-1)
for offset := 0.0; offset < 360; offset += frame.stepDeg {
radius, ok := frame.radiusAt(view, offset)
if !ok {
return 0, 0, false
}
low, high = math.Min(low, radius), math.Max(high, radius)
}
return low - starOccultationLimbUnrangedPadKM, high + starOccultationLimbUnrangedPadKM, true
}
// starOccultationSeparationCrossing 自掩甚向 direction 一侧求角距升到 radiusKM 对应角半径的时刻;角距自掩甚单调增大。
func starOccultationSeparationCrossing(greatestTT float64, direction float64, radiusKM float64, ephemeris *starLimbEphemeris, observer Observer) (float64, bool) {
value := func(tt float64) float64 {
state, ok := ephemeris.stateAt(tt, observer)
if !ok {
return math.NaN()
}
return state.separation - angularSemidiameterArcsec(radiusKM, state.distanceKM)
}
near, nearValue := greatestTT, value(greatestTT)
if !finite(nearValue) {
return 0, false
}
if nearValue >= 0 {
return greatestTT, true
}
for step := 1; float64(step)*starOccultationLimbBoundStep <= starOccultationLimbBoundSpan; step++ {
far := greatestTT + direction*float64(step)*starOccultationLimbBoundStep
farValue := value(far)
if !finite(farValue) {
return 0, false
}
if farValue < 0 {
near, nearValue = far, farValue
continue
}
for i := 0; i < 64 && math.Abs(far-near) > starOccultationLimbRootDays; i++ {
mid := (near + far) / 2
if v := value(mid); v < 0 {
near = mid
} else {
far = mid
}
}
return (near + far) / 2, true
}
return 0, false
}
type starLimbRoot struct {
tt float64
disappear bool
}
type starLimbScan struct {
segments [][]StarOccultationLimbSample
hidden [][2]float64
lowKM float64
highKM float64
stepDays float64
// grazing 为真表示星迹始终在月缘起伏带内(单段采样)。
grazing bool
}
// starOccultationLimbScanAt 按剖面起伏带把星迹分必见、必掩与带内三段,带外两端遮挡状态由构造保证;ok 为假表示剖面无值或星历失败。
func starOccultationLimbScanAt(greatestTT float64, star StarCoordinate, observer Observer, limb LunarLimb, stepDays float64, location *time.Location, inst *Instance) (starLimbScan, bool) {
lowKM, highKM, ok := starOccultationLimbRange(greatestTT, star, observer, limb, inst)
if !ok {
return starLimbScan{}, false
}
scan := starLimbScan{lowKM: lowKM, highKM: highKM, stepDays: stepDays}
ephemeris, ok := newStarLimbEphemeris(greatestTT, star, limb, inst)
if !ok {
return starLimbScan{}, false
}
outerLeft, okLeft := starOccultationSeparationCrossing(greatestTT, -1, highKM, ephemeris, observer)
outerRight, okRight := starOccultationSeparationCrossing(greatestTT, 1, highKM, ephemeris, observer)
if !okLeft || !okRight {
return starLimbScan{}, false
}
if outerLeft == greatestTT && outerRight == greatestTT {
return scan, true
}
innerLeft, okLeft := starOccultationSeparationCrossing(greatestTT, -1, lowKM, ephemeris, observer)
innerRight, okRight := starOccultationSeparationCrossing(greatestTT, 1, lowKM, ephemeris, observer)
if !okLeft || !okRight {
return starLimbScan{}, false
}
type span struct {
start, end float64
hiddenAtStart bool
hiddenAtEnd bool
forceStart, forceEnd bool
}
spans := []span{{outerLeft, innerLeft, false, true, true, true}, {innerRight, outerRight, true, false, true, true}}
if innerLeft == greatestTT && innerRight == greatestTT {
spans = []span{{outerLeft, outerRight, false, false, true, true}}
}
total, shortest := 0.0, math.Inf(1)
for _, item := range spans {
total += item.end - item.start
shortest = math.Min(shortest, item.end-item.start)
}
// 步长过大时掠掩两端都可见、中间的遮挡会整段漏掉,所以每段至少保留 64 个样本。
if limit := shortest / starOccultationLimbMinSamples; shortest > 0 && stepDays > limit {
stepDays = math.Max(limit, starOccultationLimbMinimumStep.Hours()/24)
}
if total/stepDays > starOccultationLimbMaxSamples {
stepDays = total / starOccultationLimbMaxSamples
}
scan.stepDays = stepDays
scan.grazing = len(spans) == 1
gapAt := func(tt float64) (starLimbState, float64, bool) {
state, ok := ephemeris.stateAt(tt, observer)
return state, state.separation - state.limbRadius, ok
}
var roots []starLimbRoot
for _, item := range spans {
// 剖面范围零宽时带也零长,遮挡状态在该点翻转;不产样本,避免同刻样本。
if item.end-item.start < starOccultationLimbRootDays {
if item.hiddenAtStart != item.hiddenAtEnd {
roots = append(roots, starLimbRoot{tt: item.start, disappear: item.hiddenAtEnd})
}
continue
}
count := int(math.Ceil((item.end - item.start) / stepDays))
if count < 1 {
count = 1
}
samples := make([]StarOccultationLimbSample, 0, count+1)
previousTT, previousHidden := 0.0, false
var previousGap float64
for index := 0; index <= count; index++ {
tt := item.start + (item.end-item.start)*float64(index)/float64(count)
state, gap, ok := gapAt(tt)
if !ok {
return starLimbScan{}, false
}
hidden := gap < 0
// 带两端的遮挡状态由角距保证,消除端点上的舍入抖动。
if index == 0 && item.forceStart {
hidden = item.hiddenAtStart
}
if index == count && item.forceEnd {
hidden = item.hiddenAtEnd
}
samples = append(samples, StarOccultationLimbSample{
Time: occultationTTToLocation(tt, location),
SeparationArcsec: state.separation,
LimbRadiusArcsec: state.limbRadius,
MeanRadiusArcsec: state.meanRadius,
PositionAngleDeg: state.positionAngle,
Hidden: hidden,
})
if index > 0 && hidden != previousHidden {
root := starOccultationLimbBisect(previousTT, tt, previousGap, gap, gapAt)
roots = append(roots, starLimbRoot{tt: root, disappear: hidden})
}
previousTT, previousHidden, previousGap = tt, hidden, gap
}
scan.segments = append(scan.segments, samples)
}
sort.SliceStable(roots, func(i, j int) bool { return roots[i].tt < roots[j].tt })
start := math.NaN()
for _, root := range roots {
if root.disappear {
start = root.tt
} else if !math.IsNaN(start) {
scan.hidden = append(scan.hidden, [2]float64{start, root.tt})
start = math.NaN()
}
}
return scan, true
}
// starOccultationLimbBisect 在相邻样本间求月缘间隙零点;端点被强制定号时零点可能落在端点上。
func starOccultationLimbBisect(left, right, leftGap, rightGap float64, gapAt func(float64) (starLimbState, float64, bool)) float64 {
if leftGap*rightGap > 0 {
if math.Abs(leftGap) < math.Abs(rightGap) {
return left
}
return right
}
for i := 0; i < 64 && right-left > starOccultationLimbRootDays; i++ {
mid := (left + right) / 2
_, gap, ok := gapAt(mid)
if !ok {
break
}
if (gap < 0) == (leftGap < 0) {
left, leftGap = mid, gap
} else {
right = mid
}
}
return (left + right) / 2
}
// starOccultationInfoWithLimb 有月缘时按剖面判定是否成掩,掩始、掩终取首次遮挡与最后复现;剖面无值时退回圆月面。
func starOccultationInfoWithLimb(greatestTT float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64, location *time.Location, limb LunarLimb, inst *Instance) (StarOccultationInfo, bool) {
if limb == nil {
return starOccultationInfoAtGreatest(greatestTT, star, observer, safetyMarginArcsec, location, inst)
}
info, ok := starOccultationGreatestInfo(greatestTT, star, observer, safetyMarginArcsec, location, inst)
if !ok {
return StarOccultationInfo{}, false
}
scan, ok := starOccultationLimbScanAt(greatestTT, star, observer, limb, starOccultationLimbDefaultStep.Hours()/24, location, inst)
if !ok {
return starOccultationInfoAtGreatest(greatestTT, star, observer, safetyMarginArcsec, location, inst)
}
return starOccultationInfoFromScan(info, scan, location), len(scan.hidden) > 0
}
func starOccultationInfoFromScan(info StarOccultationInfo, scan starLimbScan, location *time.Location) StarOccultationInfo {
if len(scan.hidden) == 0 {
info.Type = OccultationNone
info.Immersion, info.Emersion = time.Time{}, time.Time{}
info.ContactsComplete = false
info.LunarLimbApplied = true
return info
}
info.Type = OccultationTotal
if scan.grazing {
info.Type = OccultationGrazing
}
info.Immersion = occultationTTToLocation(scan.hidden[0][0], location)
info.Emersion = occultationTTToLocation(scan.hidden[len(scan.hidden)-1][1], location)
info.ContactsComplete = true
info.LunarLimbApplied = true
return info
}
// StarOccultationLimbProfileWithInstance 沿星迹按月缘剖面精密求解一次恒星月掩,返回全部遮挡区间与带内采样;info 只取掩甚时刻与观测者,须为 UTC 口径结果(不能传 UT1 标注副本),恒星未被遮挡时 Occultation.Type 为 OccultationNone / precise stellar occultation along the track with a lunar limb profile.
func StarOccultationLimbProfileWithInstance(info StarOccultationInfo, star StarCoordinate, limb LunarLimb, options StarOccultationLimbProfileOptions, inst *Instance) (StarOccultationLimbProfileResult, error) {
if limb == nil {
return StarOccultationLimbProfileResult{}, fmt.Errorf("%w: lunar limb profile is required", ErrInvalidOccultationInput)
}
if err := star.Validate(); err != nil {
return StarOccultationLimbProfileResult{}, err
}
if err := info.Observer.Validate(); err != nil {
return StarOccultationLimbProfileResult{}, err
}
if info.Greatest.IsZero() {
return StarOccultationLimbProfileResult{}, fmt.Errorf("%w: greatest occultation time is required", ErrInvalidOccultationInput)
}
if options.Step < 0 {
return StarOccultationLimbProfileResult{}, fmt.Errorf("%w: profile step cannot be negative", ErrInvalidOccultationInput)
}
step := options.Step
if step == 0 {
step = starOccultationLimbDefaultStep
}
if step < starOccultationLimbMinimumStep {
step = starOccultationLimbMinimumStep
}
location := info.Greatest.Location()
greatestTT := occultationTimeToTT(info.Greatest)
base, ok := starOccultationGreatestInfo(greatestTT, star, info.Observer, math.Inf(1), location, inst)
if !ok {
return StarOccultationLimbProfileResult{}, fmt.Errorf("%w: geometry is unavailable at greatest occultation", ErrInvalidOccultationInput)
}
base.TargetID = star.ID
scan, ok := starOccultationLimbScanAt(greatestTT, star, info.Observer, limb, step.Hours()/24, location, inst)
if !ok {
return StarOccultationLimbProfileResult{}, fmt.Errorf("%w: lunar limb profile has no value along the track", ErrInvalidOccultationInput)
}
profile := StarOccultationLimbProfileResult{
Occultation: starOccultationInfoFromScan(base, scan, location),
Segments: scan.segments,
ReferenceRadiusKM: moonEquatorialRadiusKM,
ReliefMinKM: scan.lowKM,
ReliefMaxKM: scan.highKM,
Step: time.Duration(math.Round(scan.stepDays * 86400 * float64(time.Second))),
}
for _, interval := range scan.hidden {
profile.Hidden = append(profile.Hidden, OccultationInterval{
Start: occultationTTToLocation(interval[0], location),
End: occultationTTToLocation(interval[1], location),
})
}
return profile, nil
}
+336
View File
@@ -0,0 +1,336 @@
package basic
// 恒星月掩按月缘剖面判定成掩:标准结果的掩始、掩终等于精密剖面的首次遮挡与最后复现,常数剖面退化为圆月面。
import (
"math"
"testing"
"time"
"b612.me/astro/moon/limb"
)
var limbTestAntares = StarCoordinate{ID: "Antares", RA: 247.3516666666667, Dec: -26.431944444444444,
Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), Frame: CoordinateFrameJ2000,
ProperMotionRACosDecMasPerYear: -10, ProperMotionDecMasPerYear: -20, ParallaxMas: 24}
func limbTestAntaresEvent(t *testing.T, lon, lat float64, profile LunarLimb) []StarOccultationInfo {
t.Helper()
start := time.Date(2026, 2, 10, 12, 0, 0, 0, time.UTC)
events, err := FindStarOccultationsWithInstance(start, start.Add(36*time.Hour), limbTestAntares, lon, lat, 0,
OccultationSearchOptions{LunarLimb: profile}, nil)
if err != nil {
t.Fatal(err)
}
return events
}
func TestStarOccultationLimbGrazeHasMultipleIntervals(t *testing.T) {
const lon, lat = 41.5907, -55.07036
if circular := limbTestAntaresEvent(t, lon, lat, nil); len(circular) != 0 {
t.Fatalf("circular limb: %d events, want none just outside the circular limit", len(circular))
}
events := limbTestAntaresEvent(t, lon, lat, limb.Default())
if len(events) != 1 || !events[0].LunarLimbApplied || events[0].Type != OccultationGrazing {
t.Fatalf("limb events = %+v, want one limb-applied graze", events)
}
profile, err := StarOccultationLimbProfileWithInstance(events[0], limbTestAntares, limb.Default(), StarOccultationLimbProfileOptions{}, nil)
if err != nil {
t.Fatal(err)
}
if len(profile.Hidden) < 2 {
t.Fatalf("graze hidden intervals = %d, want several", len(profile.Hidden))
}
first, last := profile.Hidden[0], profile.Hidden[len(profile.Hidden)-1]
if !first.Start.Equal(events[0].Immersion) || !last.End.Equal(events[0].Emersion) {
t.Errorf("standard contacts %v–%v differ from profile %v–%v", events[0].Immersion, events[0].Emersion, first.Start, last.End)
}
for index, interval := range profile.Hidden {
if !interval.End.After(interval.Start) || (index > 0 && !interval.Start.After(profile.Hidden[index-1].End)) {
t.Fatalf("interval %d not ordered: %+v", index, profile.Hidden)
}
}
for _, segment := range profile.Segments {
for _, sample := range segment {
inside := profile.Occultation.Immersion.Before(sample.Time) && sample.Time.Before(profile.Occultation.Emersion)
if sample.Hidden && !inside {
t.Fatalf("hidden sample %v outside %v–%v", sample.Time, profile.Occultation.Immersion, profile.Occultation.Emersion)
}
}
}
}
func TestStarOccultationLimbCanRemoveCircularEvent(t *testing.T) {
const lon, lat = 24.4934, -49.66243
if circular := limbTestAntaresEvent(t, lon, lat, nil); len(circular) != 1 {
t.Fatalf("circular limb: %d events, want one", len(circular))
}
if events := limbTestAntaresEvent(t, lon, lat, limb.Default()); len(events) != 0 {
t.Fatalf("limb profile: %d events, want none (star passes a limb valley)", len(events))
}
}
func TestStarOccultationConstantLimbMatchesCircular(t *testing.T) {
constant := &recordingLunarLimb{radiusKM: moonEquatorialRadiusKM}
for _, site := range [][2]float64{{41.5907, -55.2}, {24.4934, -50.5}} {
circular := limbTestAntaresEvent(t, site[0], site[1], nil)
limbed := limbTestAntaresEvent(t, site[0], site[1], constant)
if len(circular) != 1 || len(limbed) != 1 {
t.Fatalf("site %v: %d circular, %d constant-limb events", site, len(circular), len(limbed))
}
for _, pair := range [][2]time.Time{{circular[0].Immersion, limbed[0].Immersion}, {circular[0].Emersion, limbed[0].Emersion}} {
if d := math.Abs(pair[0].Sub(pair[1]).Seconds()); d > 0.01 {
t.Errorf("site %v: constant limb moved a contact by %.4f s", site, d)
}
}
}
}
func TestStarOccultationLimbProfileRejectsBadInput(t *testing.T) {
info := StarOccultationInfo{Greatest: time.Date(2026, 2, 11, 3, 47, 0, 0, time.UTC), Observer: Observer{Longitude: 41.5907, Latitude: -55.07}}
if _, err := StarOccultationLimbProfileWithInstance(info, limbTestAntares, nil, StarOccultationLimbProfileOptions{}, nil); err == nil {
t.Error("nil limb accepted")
}
if _, err := StarOccultationLimbProfileWithInstance(info, limbTestAntares, limb.Default(), StarOccultationLimbProfileOptions{Step: -time.Second}, nil); err == nil {
t.Error("negative step accepted")
}
if _, err := StarOccultationLimbProfileWithInstance(StarOccultationInfo{Observer: info.Observer}, limbTestAntares, limb.Default(), StarOccultationLimbProfileOptions{}, nil); err == nil {
t.Error("zero greatest accepted")
}
}
func TestStarLimbEphemerisMatchesExact(t *testing.T) {
observer := Observer{Longitude: 41.5907, Latitude: -55.07036}
greatest := occultationTimeToTT(time.Date(2026, 2, 11, 3, 47, 20, 0, time.UTC))
profile := limb.Default()
ephemeris, ok := newStarLimbEphemeris(greatest, limbTestAntares, profile, nil)
if !ok {
t.Fatal("ephemeris failed")
}
for _, offsetMinutes := range []float64{-200, -61.3, -7.7, 0, 3.1, 44.4, 199} {
tt := greatest + offsetMinutes/1440
got, ok := ephemeris.stateAt(tt, observer)
if !ok {
t.Fatalf("%+.1f min: interpolation failed", offsetMinutes)
}
geoRA, geoDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, nil)
geoDistance := HMoonAwayNWithInstance(tt, -1, nil)
topoRA, topoDec := moonTopocentricApparentRaDec(tt, observer, -1, nil)
topoDistance := topocentricDistanceKM(geoRA, geoDec, geoDistance, observer, TT2UTC(tt))
starRA, starDec := starApparentRaDec(tt, limbTestAntares, observer, nil)
frame := newLunarLimbFrame(profile, tt, lunarEclipseLimbVector(geoRA, geoDec, geoDistance), nil)
radiusKM, ok := frame.radiusKM(lunarEclipseLimbVector(topoRA, topoDec, topoDistance), lunarEclipseLimbVector(starRA, starDec, 1), false)
if !ok {
t.Fatal("exact limb failed")
}
separation := angularSeparationDegrees(topoRA, topoDec, starRA, starDec) * 3600
limbRadius := angularSemidiameterArcsec(radiusKM, topoDistance)
if d := math.Abs(got.separation - separation); d > 1e-3 {
t.Errorf("%+.1f min: separation off by %.2g arcsec", offsetMinutes, d)
}
if d := math.Abs(got.limbRadius - limbRadius); d > 2e-3 {
t.Errorf("%+.1f min: limb radius off by %.2g arcsec", offsetMinutes, d)
}
}
}
func TestStarOccultationLimbTypes(t *testing.T) {
deep := limbTestAntaresEvent(t, 41.5907, -57.0, limb.Default())
if len(deep) != 1 || deep[0].Type != OccultationTotal {
t.Fatalf("deep occultation = %+v, want total", deep)
}
info := StarOccultationInfo{Greatest: time.Date(2026, 2, 11, 3, 47, 15, 0, time.UTC), Observer: Observer{Longitude: 41.5907, Latitude: -55.07036 + 0.4/111.2}}
profile, err := StarOccultationLimbProfileWithInstance(info, limbTestAntares, limb.Default(), StarOccultationLimbProfileOptions{}, nil)
if err != nil {
t.Fatal(err)
}
if len(profile.Hidden) == 0 || len(profile.Segments) != 1 || profile.Occultation.Type != OccultationGrazing {
t.Fatalf("0.4 km outside the circular limit: %d intervals, %d segments, type %v; want a limb graze", len(profile.Hidden), len(profile.Segments), profile.Occultation.Type)
}
info.Observer.Latitude = -55.07036 + 0.8/111.2
profile, err = StarOccultationLimbProfileWithInstance(info, limbTestAntares, limb.Default(), StarOccultationLimbProfileOptions{}, nil)
if err != nil {
t.Fatal(err)
}
if len(profile.Hidden) != 0 || profile.Occultation.Type != OccultationNone || !profile.Occultation.Immersion.IsZero() || profile.Occultation.ContactsComplete {
t.Fatalf("missed graze reported %d intervals, type %v", len(profile.Hidden), profile.Occultation.Type)
}
}
func TestStarOccultationLimbProfileLargeStepKeepsGraze(t *testing.T) {
events := limbTestAntaresEvent(t, 41.5907, -55.07036, limb.Default())
if len(events) != 1 {
t.Fatal("graze missing")
}
profile, err := StarOccultationLimbProfileWithInstance(events[0], limbTestAntares, limb.Default(), StarOccultationLimbProfileOptions{Step: time.Hour}, nil)
if err != nil {
t.Fatal(err)
}
if len(profile.Hidden) == 0 || profile.Step >= time.Hour {
t.Fatalf("hour step: %d intervals with effective step %v, want the graze kept", len(profile.Hidden), profile.Step)
}
}
func TestStarLimbEphemerisFrameIsOrthonormal(t *testing.T) {
greatest := occultationTimeToTT(time.Date(2026, 2, 11, 3, 47, 20, 0, time.UTC))
ephemeris, ok := newStarLimbEphemeris(greatest, limbTestAntares, limb.Default(), nil)
if !ok {
t.Fatal("ephemeris failed")
}
for _, offsetMinutes := range []float64{-230, -97.3, 0, 41.1, 233} {
axisX, axisZ, ok := interpolateLocalEphemerisVectors(ephemeris.axes, greatest+offsetMinutes/1440)
if !ok {
t.Fatal("interpolation failed")
}
axisZ, _ = lunarLimbNormalize(axisZ)
axisX, _ = lunarLimbNormalize(lunarLimbReject(axisX, axisZ))
if d := math.Abs(lunarLimbDot(axisX, axisZ)); d > 1e-12 {
t.Errorf("%+.1f min: axes not orthogonal (%.2g)", offsetMinutes, d)
}
}
}
func TestPlanetOccultationLimbHandlesGraze(t *testing.T) {
start := time.Date(2024, time.August, 21, 1, 30, 0, 0, time.UTC)
end := time.Date(2024, time.August, 21, 4, 0, 0, 0, time.UTC)
const lon, edge = -30.072, 43.562592
find := func(lat float64, profile LunarLimb) []PlanetOccultationInfo {
events, err := FindPlanetOccultationsWithInstance(start, end, OccultationSaturn, lon, lat, 0, OccultationSearchOptions{LunarLimb: profile}, nil)
if err != nil {
t.Fatal(err)
}
return events
}
if circular := find(edge, nil); len(circular) != 0 {
t.Fatalf("circular edge = %+v, want no occultation just outside the circular graze line", circular)
}
limbed := find(edge, limb.Default())
if len(limbed) != 1 || !limbed[0].LunarLimbApplied || limbed[0].Type == OccultationGrazing ||
!limbed[0].ExternalEmersion.After(limbed[0].ExternalImmersion) {
t.Fatalf("limb at circular graze line = %+v, want a partial occultation with positive duration", limbed)
}
if far := find(edge+6/111.2, limb.Default()); len(far) != 0 {
t.Errorf("6 km outside the limit the limb still reports %d events", len(far))
}
constant := &recordingLunarLimb{radiusKM: moonEquatorialRadiusKM}
for _, dkm := range []float64{-3, -1} {
circular, withConstant := find(edge+dkm/111.2, nil), find(edge+dkm/111.2, constant)
if len(circular) != 1 || len(withConstant) != 1 || circular[0].Type != withConstant[0].Type {
t.Fatalf("%+.0f km: circular %v vs constant limb %v", dkm, circular, withConstant)
}
for _, pair := range [][2]time.Time{{circular[0].ExternalImmersion, withConstant[0].ExternalImmersion}, {circular[0].ExternalEmersion, withConstant[0].ExternalEmersion}} {
if d := math.Abs(pair[0].Sub(pair[1]).Seconds()); d > 0.05 {
t.Errorf("%+.0f km: constant limb moved a contact by %.3f s", dkm, d)
}
}
}
}
func TestPlanetLimbEphemerisMatchesExact(t *testing.T) {
config, _ := planetOccultationConfigFor(OccultationSaturn, nil)
observer := Observer{Longitude: -30.072, Latitude: 16.21}
greatest := occultationTimeToTT(time.Date(2024, time.August, 21, 2, 45, 0, 0, time.UTC))
ephemeris, ok := newPlanetLimbEphemeris(greatest, config, limb.Default(), nil)
if !ok {
t.Fatal("ephemeris failed")
}
target := planetOccultationLimbTarget(config, observer)
for _, offsetMinutes := range []float64{-200, -33.3, 0, 7.7, 199} {
tt := greatest + offsetMinutes/1440
state := planetOccultationStateAt(tt, config, &observer, -1, nil)
for _, internal := range []bool{false, true} {
got, ok := ephemeris.circularGap(tt, observer, internal)
want := state.externalContactMetric
if internal {
want = state.internalContactMetric
}
if !ok || math.Abs(got-want) > 2e-3 {
t.Errorf("%+.1f min internal=%v: circular gap %.4f\" want %.4f\"", offsetMinutes, internal, got, want)
}
gotLimb, okLimb := ephemeris.limbGap(tt, observer, internal)
wantLimb, okExact := occultationLimbGap(tt, limb.Default(), observer, target, internal, nil)
if !okLimb || !okExact || math.Abs(gotLimb-wantLimb) > 3e-3 {
t.Errorf("%+.1f min internal=%v: limb gap %.4f\" want %.4f\"", offsetMinutes, internal, gotLimb, wantLimb)
}
}
}
}
// occultationLimbGap 逐点精确求站心月缘接触间隙(角秒),作插值表的对拍基准。
func occultationLimbGap(tt float64, limb LunarLimb, observer Observer, target occultationLimbTarget, internal bool, inst *Instance) (float64, bool) {
geoRA, geoDec := HMoonGeocentricApparentRaDecNWithInstance(tt, -1, inst)
geoDistance := HMoonAwayNWithInstance(tt, -1, inst)
topoRA, topoDec := moonTopocentricApparentRaDec(tt, observer, -1, inst)
ut := TT2UTC(tt)
topoDistance := topocentricDistanceKM(geoRA, geoDec, geoDistance, observer, ut)
ra, dec, distance, radius, ok := target(tt)
if !ok || !finite(topoRA) || !finite(topoDec) || !finite(topoDistance) || topoDistance <= 0 {
return 0, false
}
if distance > 0 {
ra, dec = TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, ut, distance/angularDiameterAstronomicalUnitKM, observer.Height)
ra = normalizeRA(ra)
}
frame := newLunarLimbFrame(limb, tt, lunarEclipseLimbVector(geoRA, geoDec, geoDistance), inst)
view, ok := frame.viewToward(lunarEclipseLimbVector(topoRA, topoDec, topoDistance), lunarEclipseLimbVector(ra, dec, 1))
if !ok {
return 0, false
}
contact := lunarLimbContact{
frame: &frame,
view: view,
distanceKM: topoDistance,
separation: angularSeparationDegrees(topoRA, topoDec, ra, dec) * rad,
targetRadius: radius / angularDiameterArcsecPerRadian,
}
if internal {
inner, ok := contact.innerRadius(true)
if !ok {
return 0, false
}
return (contact.separation - math.Abs(inner-contact.targetRadius)) * angularDiameterArcsecPerRadian, true
}
outer, ok := contact.outerRadius()
if !ok {
return 0, false
}
return (contact.separation - outer - contact.targetRadius) * angularDiameterArcsecPerRadian, true
}
// constantRangedLunarLimb 常数半径且报告零宽范围,起伏带退化成一点。
type constantRangedLunarLimb struct{ radiusKM float64 }
func (c constantRangedLunarLimb) LimbRadiusKM(lon, lat, angle float64) (float64, bool) {
return c.radiusKM, true
}
func (c constantRangedLunarLimb) RadiusRangeKM() (float64, float64) { return c.radiusKM, c.radiusKM }
func (c constantRangedLunarLimb) AngleStepDeg() float64 { return 0.5 }
func TestStarOccultationLimbProfileZeroWidthRangeKeepsSamplesOrdered(t *testing.T) {
profileLimb := constantRangedLunarLimb{radiusKM: moonEquatorialRadiusKM}
circular := limbTestAntaresEvent(t, 41.5907, -57.0, nil)
if len(circular) != 1 {
t.Fatalf("circular events = %d, want one", len(circular))
}
profile, err := StarOccultationLimbProfileWithInstance(circular[0], limbTestAntares, profileLimb, StarOccultationLimbProfileOptions{}, nil)
if err != nil {
t.Fatal(err)
}
for index, segment := range profile.Segments {
for k := 1; k < len(segment); k++ {
if !segment[k].Time.After(segment[k-1].Time) {
t.Fatalf("segment %d sample %d: time %v not after %v", index, k, segment[k].Time, segment[k-1].Time)
}
}
}
if len(profile.Hidden) != 1 || profile.Occultation.Type != OccultationTotal {
t.Fatalf("zero-width range: %d intervals, type %v; want one total occultation", len(profile.Hidden), profile.Occultation.Type)
}
for _, pair := range [][2]time.Time{{profile.Hidden[0].Start, circular[0].Immersion}, {profile.Hidden[0].End, circular[0].Emersion}} {
if d := math.Abs(pair[0].Sub(pair[1]).Seconds()); d > 0.05 {
t.Errorf("zero-width range moved a contact by %.3f s", d)
}
}
}

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