- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
12 KiB
Explicit ephemeris instances
Package-level functions use the current ephemeris model of the process, which is the semianalytic model by default. To use several models side by side in one process, or to plug in an external ephemeris (SPK, a home-grown engine), pin the ephemeris down with astro.Context; when geometric state (position and velocity) has to be read directly, drop down to astro.Instance.
Contents
- Switching to another built-in model
- Coordinates, altitude and rise/set with a Context
- Querying events by date
- Plugging in an external ephemeris
- Stacking multiple sources
- Engine contract
- Limits
Switching to another built-in model
astro.NewContext creates an independent context, and the model is fixed when the context is created:
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) // the model used by default builds
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)
330.64554489 -11.99819781
330.64554785 -11.99820252
Once a context is created its model is fixed, and a later astro.SetModel does not affect it. SetModel changes the default model of the package-level functions and accepts only the two built-in models; any other model returns astro.ErrUnknownModel and leaves the global state unchanged. A custom engine must be passed explicitly to a Context or an Instance. NewContext returns ErrUnknownModel for an unknown model and ErrInvalidEngine when the engines do not fit the model (an engine passed with a built-in model, or ModelCustom without exactly one non-nil engine); NewModelInstance accepts only built-in models.
Coordinates, altitude and rise/set with a Context
The methods returned by the accessors take the same arguments and return the same values as the package-level functions of the same name, so sun.Xxx becomes ctx.Sun().Xxx:
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
// The Sun: geocentric apparent right ascension/declination, topocentric altitude/azimuth
ra, dec := ctx.Sun().ApparentRaDec(date)
alt := ctx.Sun().Altitude(date, lon, lat)
az := ctx.Sun().Azimuth(date, lon, lat)
fmt.Printf("Sun ra=%.6f dec=%.6f alt=%.6f az=%.6f\n", ra, dec, alt, az)
// The Moon: use GeocentricApparentRaDec for geocentric coordinates, topocentric quantities take the longitude and latitude directly
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("Moon ra=%.6f dec=%.6f alt=%.6f rise=%s err=%v\n",
moonRa, moonDec, moonAlt, moonRise.Format("15:04:05"), err)
// Planets are reached through accessors named after their packages
marsRa, marsDec := ctx.Mars().ApparentRaDec(date)
marsAlt := ctx.Mars().Altitude(date, lon, lat)
fmt.Printf("Mars ra=%.6f dec=%.6f alt=%.6f\n", marsRa, marsDec, marsAlt)
Sun ra=330.645545 dec=-11.998198 alt=37.682918 az=171.221286
Moon ra=327.197499 dec=-14.621205 alt=34.596340 rise=06:59:57 err=<nil>
Mars ra=322.193276 dec=-16.023967 alt=34.060823
The instant is 2026-02-17 12:00 CST, and the site is 116.4° east and 39.9° north. RiseTime returns an error instead of a zero time when it finds no rise or set within the local civil day.
| Entry point | Package | Purpose |
|---|---|---|
Sun() |
sun |
Geocentric coordinates, topocentric altitude/azimuth/zenith distance, rise and set and twilight, apparent diameter, physical ephemeris |
Moon() |
moon |
Geocentric and topocentric coordinates, altitude/azimuth, rise and set, lunar phase, geocentric distance, libration |
Mercury() … Neptune() |
The planet packages | Apparent coordinates, altitude/azimuth, rise and set, phase and apparent magnitude, events such as opposition, conjunction and greatest elongation |
Eclipse() |
eclipse |
Solar and lunar eclipse queries, local contact times, eclipse paths |
Phenomena() |
phenomena |
Ecliptic longitude crossings, stations, closest approach, rise and set at any altitude, heliacal visibility |
GeoJSON() |
geojson |
Map data for celestial events |
The ...N methods use the fast table, for example ctx.Sun().AltitudeN(date, lon, lat, n) and ctx.Moon().TrueLoN(date, n); see Accuracy and performance for the meaning of n.
Querying events by date
Event queries also hang off the modules:
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)
true 2026-03-03 19:33 CST total
2026-03-03 19:33 CST total
ok is true only when a lunar eclipse occurs on that date; the example date 2026-03-03 has one. Filtering and searching both use the instance of that context. The Saros tables of the built-in models are selected according to the instance model; a custom engine can describe an arbitrary trajectory, so no Saros number is inferred and HasSaros is false in the result.
Plugging in an external ephemeris
spk.Open reads a JPL SPK kernel and passes it to NewContext as an astro.Engine; afterwards the calls are exactly the same as with a built-in model:
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)
}
// Reuse the date above (2026-02-17 12:00 CST) and the longitude and latitude
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("Sun ra=%.6f dec=%.6f alt=%.6f\n", ra, dec, alt)
fmt.Printf("Mars ra=%.6f dec=%.6f alt=%.6f\n", marsRa, marsDec, marsAlt)
fmt.Printf("Moon alt=%.6f\n", moonAlt)
Sun ra=330.645545 dec=-11.998198 alt=37.682918
Mars ra=322.193276 dec=-16.023967 alt=34.060823
Moon alt=34.596325
The results agree closely with the built-in semianalytic model at the same instant; here the lunar altitude differs by about 0.05″.
Use Instance instead when the raw geometric state (position and velocity) is needed:
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/day\nerr=%v coverage=%.1f..%.1f ok=%v\n",
pos[0], pos[1], pos[2], vel[0], vel[1], vel[2], err, start, end, ok)
pos=312305.1,-204135.4,-98208.0 km
vel=48600.2,64310.3,36543.8 km/day
err=<nil> coverage=2396752.5..2506352.5 ok=true
State takes TDB epochs and returns the ICRF geometric position (km) and velocity (km/day). Coverage gives the usable epoch interval of that body, and going outside it returns ErrEphemerisOutOfRange.
Stacking multiple sources
astro.NewStackInstance composes several engines in argument order, and the source passed first has priority. Small-body kernels often provide only the state of the target relative to the Sun, and Stack composes a state of the target relative to the solar-system barycenter across sources:
de440, _ := spk.Open("de440s.bsp")
ceres, _ := spk.Open("ceres.bsp")
defer de440.Close()
defer ceres.Close()
stack := astro.NewStackInstance(de440, ceres)
// A small-body kernel usually covers a narrow range, so the query epoch must fall inside its coverage
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 day err=%v\n", dir[0], dir[1], dir[2], dist, tau, err)
0.98097637,0.17021858,-0.09333272
dist=417020638 km tau=0.01610 day err=<nil>
astro.Body is an int, so any NAIF identifier can be written directly; Ceres is 20000001. The Ceres kernel in this example covers only 2026-01-01 to 01-03 (stack.Coverage returns 2461041.5..2461043.5); going outside the coverage returns ErrEphemerisOutOfRange and never silently switches to another source.
Engine contract
Engine.State(target, center, jdTDB): ICRF geometric position (km) and velocity (km/day), with TDB epochs. A custom engine must implement it.- The optional
ApparentEngine.Apparent(target, jdTT): geocentric ICRF unit apparent direction, distance (km) and light time (days) at a TT epoch. An engine that implements onlyState(such as SPK) goes through the generic vector apparent-place chain: iterate the light time, then add solar gravitational deflection and relativistic aberration; this requires the engine to provide states of the Earth, the Sun and the target relative to the solar-system barycenter (BodySSB). - Engines are called concurrently and must be read-only and reentrant; the library does not start goroutines.
- A failure of a custom engine does not fall back to the built-in tables: scalar entry points return
NaN, theMaximumof a lunar eclipse core isNaNand each phase flag isfalse, and date queries report a miss. - Scalar coordinates and the
basiclunar eclipse entry points take TT epochs; the geometric engine currently approximates TDB with TT, so callInstance.Statedirectly when strict TDB is required.
Limits
EphemerisLegacyinstances have scalar entry points only and no geometric engine:Instance.Statereturnsastro.ErrEphemerisNoEngine, andCoverageandApparentreport unavailable. Use the semianalytic model or a custom engine when positions and velocities are needed.- An explicit instance only decides which ephemeris is used, and does not guarantee a smaller binary: both built-in tables are linked into the program.
- The usable range of the semianalytic model is TT JD
-1931455.0 … 5373545.0, and evaluation still extrapolates outside it; the modern planetary segment is JD2086295.0 … 2816795.0. See Accuracy and performance for the errors. Stack.Coverageis the union of the source intervals, may contain gaps in between, and does not guarantee that every body needed by a center chain (the path of centers Stack searches for between its sources to compose a state) is available; rely on whatStatereturns at a specific epoch.- Most
basicentry points that support instances carry theWithInstancesuffix, and then-parameterised ones carryNWithInstance; passingnilmeans the current global model of the process. The exceptions are the shadow-radius entryLunarEclipseWithLunarLimband the 18 atmospheric rise/set entries, which take*Instanceas their last parameter. Direct calls to these entry points are normally unnecessary.