Files
astro/doc/manual/en/phenomena.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

12 KiB

Generic sky events and heliacal visibility

中文 | Back to README

The phenomena package searches events between any two bodies by geocentric apparent ecliptic longitude and latitude, computes morning and evening first and last visibility for planets and stars, and additionally provides rise and set at any altitude plus perihelion, aphelion and node passages. Package-level entry points use the process's current ephemeris model, and ctx.Phenomena() can fix an ephemeris instance; only a star's rise and set at any altitude is computed from the apparent coordinates passed in and does not read the ephemeris.

Contents

Quick example

start := time.Date(2020, 10, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2021, 3, 1, 0, 0, 0, 0, time.UTC)

events := phenomena.ClosestApproaches(phenomena.Jupiter, phenomena.Saturn, 1, start, end)
fmt.Println(events[0].Time.UTC().Format("2006-01-02 15:04"), fmt.Sprintf("%.4f", events[0].Value))
2020-12-21 18:21 0.1018

ClosestApproaches returns the local minima of the angular separation of the two bodies within the window, where Value is the separation at that instant in degrees. It returns an empty slice when the window contains no local minimum below maxSeparation.

To fix the ephemeris instance, use the module method instead:

ctx, err := astro.NewContext(astro.ModelLegacy)
if err != nil {
	panic(err)
}
events = ctx.Phenomena().ClosestApproaches(phenomena.Jupiter, phenomena.Saturn, 1, start, end)

Pairwise events

Entry Event Event.Value
LongitudeCrossings(body, lon, start, end) apparent longitude passes through lon apparent longitude at the event instant (degrees)
Stations(body, start, end) stations of apparent longitude (planets) apparent longitude at the station (degrees)
AspectCrossings(a, b, angle, start, end) lon(a) - lon(b) = angle; 0 is conjunction in longitude, 180 opposition the actual longitude difference (degrees)
ClosestApproaches(a, b, max, start, end) local minima of the angular separation of two bodies angular separation (degrees)
StarApproaches(body, star, max, start, end) local minima of the angular separation from a star angular separation (degrees)

Each entry accepts only the parameters listed in the table above, and results are returned in ascending time order. Stations defined by right ascension in the per-planet packages use a different convention from the longitude stations here, so the times can differ by several hours.

// first two Mercury stations of 2026
stations := phenomena.Stations(phenomena.Mercury,
	time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC))
for _, e := range stations {
	fmt.Println(e.Time.UTC().Format("01-02 15:04"), fmt.Sprintf("%.3f", e.Value))
}
02-26 06:48 352.565
03-20 19:32 338.491
// apparent solar longitude passing through 0° (vernal equinox)
crossings := phenomena.LongitudeCrossings(phenomena.Sun, 0,
	time.Date(2026, 3, 18, 0, 0, 0, 0, time.UTC), time.Date(2026, 3, 24, 0, 0, 0, 0, time.UTC))
fmt.Println(crossings[0].Time.UTC().Format("2006-01-02 15:04"))
2026-03-20 14:45

Star coordinates

Both the angular separation of a planet and a star and star heliacal visibility require a StarCoordinate; the embedded 9100-star catalog can be queried directly:

Entry Input
StarCoordinateByName(name) catalog Chinese name, for example "天狼星"
StarCoordinateByHR(hr) HR number
StarCoordinateFromStarData(star) a catalog record such as one obtained from star.StarDataByHR
star, err := phenomena.StarCoordinateByName("天狼星")
fmt.Println(err, star.ID, fmt.Sprintf("%.6f %.6f %.2f %.2f", star.RA, star.Dec,
	star.ProperMotionRACosDecMasPerYear, star.ProperMotionDecMasPerYear))
<nil> 天狼 101.287083 -16.716111 -553.00 -1205.00

RA and Dec are in degrees, and Epoch and Frame are required; the proper-motion fields (ProperMotionRACosDecMasPerYear and friends) follow the catalog convention dRA·cos(Dec), in milliarcseconds per year. ParallaxMas (or the equivalent DistanceLightYear) gives the distance, and RadialVelocityKmPerSecond gives the radial velocity; when the distance is missing the propagation is two-dimensional, and the radial velocity is included only when a distance is present.

Heliacal visibility

PlanetHeliacalEvents(body, start, end, lon, lat, options) and StarHeliacalEvents(star, magnitude, start, end, lon, lat, options) scan the morning and evening intervals day by day while the solar altitude runs from -18° to 0°, derive the naked-eye limiting magnitude from a sky-brightness model, and compare it with the target apparent magnitude (Mallama & Hilton 2018 for planets). They return four kinds of event:

Constant Meaning
HeliacalMorningFirst morning first visibility (heliacal rising)
HeliacalMorningLast morning last visibility
HeliacalEveningFirst evening first visibility
HeliacalEveningLast evening last visibility (heliacal setting)

In HeliacalEvent, Time is the best visible instant of that twilight, TargetAltitude and SunAltitude are the geometric altitudes of the target and the Sun at that instant (degrees), and Margin is the limiting magnitude minus the target magnitude.

HeliacalOptions Default Description
ExtinctionCoefficient 0.25 atmospheric extinction, magnitudes per airmass
MinTargetAltitude 0.5 minimum geometric altitude of the target (degrees)
Moonlight false include scattered moonlight (Krisciunas-Schaefer)
events := phenomena.PlanetHeliacalEvents(phenomena.Venus,
	time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 12, 31, 0, 0, 0, 0, time.UTC),
	31.23, 30.04, phenomena.HeliacalOptions{})
for _, e := range events {
	fmt.Println(e.Kind, e.Time.UTC().Format("2006-01-02"), fmt.Sprintf("margin=%.2f", e.Margin))
}
3 2025-03-18 margin=0.10
0 2025-03-25 margin=0.58
1 2025-12-01 margin=0.10

Kind prints as the constant's ordinal, so compare against the constants themselves when testing. LimitingMagnitude(targetAltitude, sunAltitude, separation, extinction) gives the limiting magnitude at a single instant on a moonless night, from the same source as the event test:

fmt.Println(fmt.Sprintf("%.3f", phenomena.LimitingMagnitude(20, -12, 60, 0.25)))
3.408

Results are sensitive to the extinction coefficient, and a spread of one or two days in the visible date is normal.

Rise and set at any altitude

AltitudeCrossing(body, date, lon, lat, height, altitude, options) finds the rise and set times at which the Sun, Moon or a planet first crosses the apparent altitude altitude within the local civil day containing date. StarAltitudeCrossing(date, ra, dec, lon, lat, altitude, options) performs the same computation for a star's apparent coordinates; it does not read the ephemeris and has no module method.

AltitudeOptions Default Description
Limb LimbUpper upper limb for the Sun and Moon, or LimbCenter and LimbLower; ignored for planets and stars
PressureHPa / TemperatureC standard atmosphere (the 0 value) specify measured weather
NoRefraction false when true, compare geometric altitudes
AltitudeCrossingResult Description
Rise, Set rise and set times, zero when there is no event
HasRise, HasSet whether a rise or set occurs that day
State AltitudeCrosses, AltitudeAlwaysAbove, AltitudeAlwaysBelow or AltitudeTangent (the extremum differs from the target altitude by less than 1″)
MinResidual, MaxResidual minimum and maximum of the altitude minus the target altitude that day (degrees)
cst := time.FixedZone("CST", 8*3600)
r, err := phenomena.AltitudeCrossing(phenomena.Sun, time.Date(2026, 3, 21, 12, 0, 0, 0, cst),
	116.4, 39.9, 0, -18, phenomena.AltitudeOptions{})
fmt.Println(err, r.State, r.Rise.In(cst).Format("15:04:05"), r.Set.In(cst).Format("15:04:05"),
	fmt.Sprintf("%.3f %.3f", r.MinResidual, r.MaxResidual))
<nil> 0 04:44:54 19:59:16 -31.806 68.602
// 89°N polar day: the Sun stays above -18° all day
polar, err := phenomena.AltitudeCrossing(phenomena.Sun, time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC),
	0, 89, 0, -18, phenomena.AltitudeOptions{})
fmt.Println(err, polar.State, polar.HasRise, polar.HasSet, fmt.Sprintf("%.3f", polar.MinResidual))
<nil> 1 false false 40.737
// rise and set of Sirius through -0.833°
sirius, err := phenomena.StarAltitudeCrossing(time.Date(2026, 1, 1, 12, 0, 0, 0, cst),
	star.RA, star.Dec, 116.4, 39.9, -0.833, phenomena.AltitudeOptions{Limb: phenomena.LimbCenter})
fmt.Println(err, sirius.State, sirius.Rise.In(cst).Format("15:04:05"), sirius.Set.In(cst).Format("15:04:05"))
<nil> 0 19:04:31 05:27:52

height only affects lunar parallax. When a depressed horizon is required, the caller subtracts the horizon dip from altitude. When the body is not the Sun, Moon or a planet (for example phenomena.Earth), or a star's apparent coordinates are not finite, the function returns basic.ErrInvalidObservationInput.

Apsides and nodes

Constant Event
Perihelion, Aphelion perihelion, aphelion
AscendingNode, DescendingNode ascending and descending node passages through the mean ecliptic of epoch

Apsides(body, start, end) accepts a planet or the Earth (phenomena.Earth), while Nodes(body, start, end) accepts only planets and returns an empty slice for the Earth. In OrbitalEvent, Time is the event instant, Kind is a constant from the table above, and DistanceAU is the heliocentric distance at that instant (AU).

apsides := phenomena.Apsides(phenomena.Earth,
	time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC))
for _, e := range apsides {
	fmt.Println(e.Kind, e.Time.UTC().Format("2006-01-02 15:04"), fmt.Sprintf("%.9f AU", e.DistanceAU))
}
nodes := phenomena.Nodes(phenomena.Jupiter,
	time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC))
for _, e := range nodes {
	fmt.Println(e.Kind, e.Time.UTC().Format("2006-01-02 15:04"), fmt.Sprintf("%.6f AU", e.DistanceAU))
}
0 2026-01-03 09:15 0.983302049 AU
1 2026-07-06 09:30 1.016643962 AU
3 2020-02-25 18:09 5.207056 AU
2 2025-09-19 11:58 5.174312 AU

The extremum is taken on the heliocentric distance itself: the Sun's reflex motion about the barycenter makes the heliocentric distance of Uranus and Neptune show several small extrema near perihelion, so when several extrema appear in one span only the one with the smallest heliocentric distance within 0.3 orbital periods before and after is returned, which can differ from the osculating perihelion by several years.

API reference

The methods of ctx.Phenomena() match the package-level functions of the same name (except StarAltitudeCrossing); NewWithInstance creates a module bound to a given ephemeris instance. Parameters and units are in the sections above.

Parameter and return conventions

  • Angles are always in degrees, OrbitalEvent.DistanceAU is in AU, and magnitude in StarHeliacalEvents is V magnitude.
  • Time parameters are interpreted as absolute instants (their UTC fields are read), and returned instants are converted to the time zone of start (date for the rise and set entries).
  • Event searches return an empty slice rather than an error when there is no result; only rise and set at any altitude returns basic.ErrInvalidObservationInput for invalid input.
  • Event searches use the ephemeris, which a module method can fix; StarAltitudeCrossing, StarCoordinate* and LimitingMagnitude are independent of the ephemeris.
  • For differences in convention such as planetary apparent magnitude and right-ascension stations see the planet manual; for Sun and Moon rise and set and observing-angle semantics see Sun and Moon.