Files
astro/doc/manual/ephemeris.md
T
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

220 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 显式星历实例
[English](en/ephemeris.md) | [返回 README](../../README.md)
包级函数使用进程当前的星历模型,默认是半解析模型。要在同一个进程里并用多套模型,或接入外部星历(SPK、自研引擎),就用 `astro.Context` 把星历固定下来;需要直接读取几何状态(位置和速度)时,再用底层的 `astro.Instance`。
## 目录
- [换用另一套内置模型](#换用另一套内置模型)
- [用 Context 求坐标、高度与升落](#用-context-求坐标高度与升落)
- [按日期查询事件](#按日期查询事件)
- [接入外部星历](#接入外部星历)
- [多源叠加](#多源叠加)
- [引擎契约](#引擎契约)
- [限制](#限制)
## 换用另一套内置模型
`astro.NewContext` 创建独立上下文,模型在创建时固定:
```go
import (
"fmt"
"time"
"b612.me/astro"
)
date := time.Date(2026, 2, 17, 12, 0, 0, 0, time.FixedZone("CST", 8*3600))
semi, err := astro.NewContext(astro.ModelSemianalytic) // 默认构建使用的模型
if err != nil {
panic(err)
}
legacy, err := astro.NewContext(astro.ModelLegacy)
if err != nil {
panic(err)
}
ra1, dec1 := semi.Sun().ApparentRaDec(date)
ra2, dec2 := legacy.Sun().ApparentRaDec(date)
fmt.Printf("%.8f %.8f\n", ra1, dec1)
fmt.Printf("%.8f %.8f\n", ra2, dec2)
```
```text
330.64554489 -11.99819781
330.64554785 -11.99820252
```
上下文一旦创建就固定模型,之后的 `astro.SetModel` 不会影响它。`SetModel` 改的是包级函数的默认模型,只接受两个内置模型,其他模型返回 `astro.ErrUnknownModel` 且不改动全局状态;自定义引擎必须显式传给 `Context` 或 `Instance`。`NewContext` 对未知模型返回 `ErrUnknownModel`,引擎个数或取值不符(内置模型带引擎、`ModelCustom` 不是恰好一个非 nil 引擎)返回 `ErrInvalidEngine`;`NewModelInstance` 只接受内置模型。
## 用 Context 求坐标、高度与升落
访问器返回的方法与同名包级函数参数、返回值一致,把 `sun.Xxx` 换成 `ctx.Sun().Xxx` 即可:
```go
ctx, err := astro.NewContext(astro.ModelSemianalytic)
if err != nil {
panic(err)
}
date := time.Date(2026, 2, 17, 12, 0, 0, 0, time.FixedZone("CST", 8*3600))
lon, lat := 116.4, 39.9
// 太阳:地心视赤经/赤纬,站心高度角/方位角
ra, dec := ctx.Sun().ApparentRaDec(date)
alt := ctx.Sun().Altitude(date, lon, lat)
az := ctx.Sun().Azimuth(date, lon, lat)
fmt.Printf("太阳 ra=%.6f dec=%.6f alt=%.6f az=%.6f\n", ra, dec, alt, az)
// 月球:地心坐标用 GeocentricApparentRaDec,站心量直接带经纬度
moonRa, moonDec := ctx.Moon().GeocentricApparentRaDec(date)
moonAlt := ctx.Moon().Altitude(date, lon, lat)
moonRise, err := ctx.Moon().RiseTime(date, lon, lat, 0, true)
fmt.Printf("月球 ra=%.6f dec=%.6f alt=%.6f 升起=%s err=%v\n",
moonRa, moonDec, moonAlt, moonRise.Format("15:04:05"), err)
// 行星按包名取访问器
marsRa, marsDec := ctx.Mars().ApparentRaDec(date)
marsAlt := ctx.Mars().Altitude(date, lon, lat)
fmt.Printf("火星 ra=%.6f dec=%.6f alt=%.6f\n", marsRa, marsDec, marsAlt)
```
```text
太阳 ra=330.645545 dec=-11.998198 alt=37.682918 az=171.221286
月球 ra=327.197499 dec=-14.621205 alt=34.596340 升起=06:59:57 err=<nil>
火星 ra=322.193276 dec=-16.023967 alt=34.060823
```
时刻为 2026-02-17 12:00 CST,观测点东经 116.4°、北纬 39.9°。`RiseTime` 在当天当地民用日内找不到升落时返回错误,不是零值时刻。
| 入口 | 所属包 | 用途 |
| --- | --- | --- |
| `Sun()` | `sun` | 地心坐标、站心高度/方位/天顶距、升落与朦影、视直径、物理星历 |
| `Moon()` | `moon` | 地心与站心坐标、高度/方位、升落、月相、地心距、天平动 |
| `Mercury()` … `Neptune()` | 各行星包 | 视坐标、高度/方位、升落、相位与视星等、冲合和大距等事件 |
| `Eclipse()` | `eclipse` | 日月食查询、地方接触时刻、食带路径 |
| `Phenomena()` | `phenomena` | 黄经穿越、留、最近角距、任意高度升落、偕日可见 |
| `GeoJSON()` | `geojson` | 天象地图数据 |
`...N` 方法走快速表,例如 `ctx.Sun().AltitudeN(date, lon, lat, n)`、`ctx.Moon().TrueLoN(date, n)`;`n` 的含义见[精度与性能](accuracy.md)。
## 按日期查询事件
事件查询也挂在模块上:
```go
date := time.Date(2026, 3, 3, 12, 0, 0, 0, time.FixedZone("CST", 8*3600))
info, ok := ctx.Eclipse().LunarEclipseOnDate(date)
next := ctx.Eclipse().NextLunarEclipse(date)
fmt.Println(ok, info.Maximum.Format("2006-01-02 15:04 MST"), info.Type)
fmt.Println(next.Maximum.Format("2006-01-02 15:04 MST"), next.Type)
```
```text
true 2026-03-03 19:33 CST total
2026-03-03 19:33 CST total
```
日期当天有月食时 `ok` 才为 `true`;示例日期 2026-03-03 当天有月食。筛选与搜索均使用该上下文的实例。内置模型的沙罗表按实例模型选择;自定义引擎可以描述任意轨迹,因此不推断沙罗编号,结果中 `HasSaros` 为 `false`。
## 接入外部星历
`spk.Open` 读取 JPL SPK 核,作为 `astro.Engine` 传给 `NewContext`,之后的调用方式和内置模型完全相同:
```go
import "b612.me/astro/planet/spk"
kernel, err := spk.Open("de440s.bsp")
if err != nil {
panic(err)
}
defer kernel.Close()
custom, err := astro.NewContext(astro.ModelCustom, kernel)
if err != nil {
panic(err)
}
// 沿用上面的 date(2026-02-17 12:00 CST)与经纬度
ra, dec := custom.Sun().ApparentRaDec(date)
alt := custom.Sun().Altitude(date, lon, lat)
marsRa, marsDec := custom.Mars().ApparentRaDec(date)
marsAlt := custom.Mars().Altitude(date, lon, lat)
moonAlt := custom.Moon().Altitude(date, lon, lat)
fmt.Printf("太阳 ra=%.6f dec=%.6f alt=%.6f\n", ra, dec, alt)
fmt.Printf("火星 ra=%.6f dec=%.6f alt=%.6f\n", marsRa, marsDec, marsAlt)
fmt.Printf("月球 alt=%.6f\n", moonAlt)
```
```text
太阳 ra=330.645545 dec=-11.998198 alt=37.682918
火星 ra=322.193276 dec=-16.023967 alt=34.060823
月球 alt=34.596325
```
同一时刻与内置半解析基本一致,这里月球高度差约 `0.05″`。
需要原始几何状态(位置和速度)时改用 `Instance`:
```go
inst := astro.NewInstance(kernel)
jdTDB := calendar.Date2JD(astro.TDBFromTT(date))
pos, vel, err := inst.State(astro.BodyMoon, astro.BodyEarth, jdTDB)
start, end, ok := inst.Coverage(astro.BodyMoon)
fmt.Printf("pos=%.1f,%.1f,%.1f km\nvel=%.1f,%.1f,%.1f km/日\nerr=%v coverage=%.1f..%.1f ok=%v\n",
pos[0], pos[1], pos[2], vel[0], vel[1], vel[2], err, start, end, ok)
```
```text
pos=312305.1,-204135.4,-98208.0 km
vel=48600.2,64310.3,36543.8 km/日
err=<nil> coverage=2396752.5..2506352.5 ok=true
```
`State` 接收 TDB 历元,返回 ICRF 几何位置(km)与速度(km/日)。`Coverage` 给出该天体的可用历元区间,越界返回 `ErrEphemerisOutOfRange`。
## 多源叠加
`astro.NewStackInstance` 按先后顺序合成多个引擎,先传入的源优先。小天体核常常只给“目标相对太阳”的状态,Stack 会跨源拼出目标相对太阳系质心的状态:
```go
de440, _ := spk.Open("de440s.bsp")
ceres, _ := spk.Open("ceres.bsp")
defer de440.Close()
defer ceres.Close()
stack := astro.NewStackInstance(de440, ceres)
// 小天体核的覆盖往往很窄,查询历元必须落在覆盖内
smallDate := time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC)
jdTT := calendar.Date2JD(astro.TTFromUTC(smallDate))
dir, dist, tau, err := stack.Apparent(astro.Body(20000001), jdTT)
fmt.Printf("%.8f,%.8f,%.8f\ndist=%.0f km tau=%.5f 日 err=%v\n", dir[0], dir[1], dir[2], dist, tau, err)
```
```text
0.98097637,0.17021858,-0.09333272
dist=417020638 km tau=0.01610 日 err=<nil>
```
`astro.Body` 是 `int`,任意 NAIF 编号可以直接写,Ceres 是 `20000001`。示例这份 Ceres 核只覆盖 2026-01-01 至 01-03(`stack.Coverage` 返回 `2461041.5..2461043.5`),超出覆盖返回 `ErrEphemerisOutOfRange`,不会静默改用别的源。
## 引擎契约
- `Engine.State(target, center, jdTDB)`:ICRF 几何位置(km)与速度(km/日),历元为 TDB。自定义引擎必须实现它。
- 可选的 `ApparentEngine.Apparent(target, jdTT)`:TT 历元的地心 ICRF 单位视方向、距离(km)与光行时(日)。只实现 `State` 的引擎(如 SPK)走通用矢量视位置链:迭代光行时,加太阳引力偏折与相对论光行差;这需要引擎能给出地球、太阳和目标相对太阳系质心(`BodySSB`)的状态。
- 引擎会被并发调用,必须只读、可重入;库不会启动 goroutine。
- 自定义引擎的失败不回退到内置表:标量入口返回 `NaN`,月食核心的 `Maximum` 为 `NaN` 且各阶段标志为 `false`,日期查询返回未找到。
- 标量坐标和 `basic` 月食入口按 TT 取历元;几何引擎目前以 TT 近似 TDB,需要严格 TDB 时直接调用 `Instance.State`。
## 限制
- `EphemerisLegacy` 实例只有标量入口,没有几何引擎:`Instance.State` 返回 `astro.ErrEphemerisNoEngine`,`Coverage` 与 `Apparent` 报告不可用。需要位置和速度时用半解析模型或自定义引擎。
- 显式实例只决定用哪份星历,不保证减小二进制体积:两张内置表都会链接进程序。
- 半解析模型的可用区间是 TT JD `-1931455.0 … 5373545.0`,越界仍外推;行星现代段为 JD `2086295.0 … 2816795.0`。误差见[精度与性能](accuracy.md)。
- `Stack.Coverage` 是各源区间的并集,中间可能有断档,也不保证中心链(Stack 为拼接状态在源之间搜索的中心传递路径)所需的每个天体都可用;以 `State` 在具体历元的返回值为准。
- `basic` 中支持实例的入口多数带 `WithInstance` 后缀,带 `n` 参数的入口带 `NWithInstance`,传 `nil` 表示使用进程当前的全局模型。例外是影半径入口 `LunarEclipseWithLunarLimb` 和 18 个大气升落入口,它们把 `*Instance` 放在参数末尾。一般不需要直接调用这些入口。