Files
astro/doc/manual/en/orbit.md
T
b612 16c62a97d5 feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
2026-09-23 18:55:12 +08:00

25 KiB

Generic Small-Body Orbits

中文 | Back to README

orbit propagates heliocentric two-body positions from orbital elements. It supports asteroids, comets, dwarf planets, and custom hypothetical orbits.

The seven major planets are still computed by their own packages using built-in VSOP87 analytical terms.

orbit.Elements supports two common forms:

  • classical elliptical elements: A/E/I/Omega/W/M0
  • perihelion form: Q/E/I/Omega/W/TpJD, useful for comets and high-eccentricity orbits

The reference frame of orbit.Elements is always the J2000 mean ecliptic and mean equinox. The examples below use one set of Ceres elements.

Topocentric and rise/set helpers take east-positive longitude, north-positive latitude, and height in meters.

The positional and topocentric quantities here also work together with the interfaces documented in the star and coordinate tools manuals.

Contents

Calculating the position of Ceres from orbital elements

package main

import (
	"fmt"
	"log"
	"time"

	"b612.me/astro/orbit"
)

func main() {
	cst := time.FixedZone("CST", 8*3600)
	when := time.Date(2025, 11, 21, 20, 0, 0, 0, cst)
	ceres := orbit.Elements{
		EpochJD: 2461000.5, A: 2.765615651508659, E: 0.07957631994408416,
		I: 10.58788658206854, Omega: 80.24963090816965,
		W: 73.29975464616518, M0: 231.5397330043706,
	}
	pos := orbit.ApparentGeocentricEquatorial(when, ceres)
	fmt.Printf("RA=%.6f Dec=%.6f deg distance=%.6f AU\n", pos.RA, pos.Dec, pos.Distance)
	rise, err := orbit.RiseTime(when, ceres, 121.4737, 31.2304, 20, true)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(rise.Format(time.RFC3339))
}

Elements use the J2000 mean ecliptic frame and a TT/TDB Julian-day epoch. This is heliocentric two-body propagation; perturbation errors need separate evaluation over long spans or near planetary encounters.

API Reference

Orbital Elements

Name Purpose Units and convention
Elements Heliocentric two-body conic elements EpochJD/TpJD are TT/TDB Julian days; A/Q in AU, I/Omega/W/M0 in degrees, E dimensionless; ADot…MDot are per-day rates that apply to the classical elliptical form only
MeanMotion Mean angular rate degrees/day; NaN for parabolic and hyperbolic cases; a non-zero MDot is used directly
MeanAnomaly Mean anomaly degrees ([0,360)); NaN for parabolic and hyperbolic cases
TrueAnomaly True anomaly degrees ([0,360)); defined for elliptical, parabolic, and hyperbolic orbits, NaN for invalid elements

Mean anomaly and true anomaly are solved from the same element set, and MDot can replace the default mean motion:

fmt.Println(orbit.MeanMotion(ceres), orbit.MeanAnomaly(when, ceres), orbit.TrueAnomaly(when, ceres))

// Parabolic and hyperbolic orbits only accept the perihelion form; mean motion and mean anomaly are undefined.
parabolic := orbit.Elements{Q: 0.9, E: 1, I: 30, Omega: 40, W: 50, TpJD: 2461000.5}
hyperbolic := orbit.Elements{Q: 1.2, E: 1.05, I: 30, Omega: 40, W: 50, TpJD: 2461000.5}
fmt.Println(orbit.MeanMotion(parabolic), orbit.MeanAnomaly(when, parabolic))
fmt.Println(orbit.TrueAnomaly(when, parabolic), orbit.TrueAnomaly(when, hyperbolic))

The complete example exercises both the elliptical and the perihelion element form:

package main

import (
	"fmt"
	"time"

	"b612.me/astro/orbit"
)

func main() {
	// Classical elliptical elements for 1 Ceres, referenced to J2000 mean ecliptic/equinox.
	ceres := orbit.Elements{
		EpochJD: 2461000.5,
		A:       2.765615651508659,
		E:       0.07957631994408416,
		I:       10.58788658206854,
		Omega:   80.24963090816965,
		W:       73.29975464616518,
		M0:      231.5397330043706,
	}
	ceresPos := orbit.ApparentGeocentricEquatorial(
		time.Date(2025, 11, 12, 0, 0, 0, 0, time.UTC),
		ceres,
	)
	fmt.Printf("ceres ra=%.6f dec=%.6f distance=%.6f\n", ceresPos.RA, ceresPos.Dec, ceresPos.Distance)

	// Halley's Comet example using perihelion distance Q and perihelion passage time TpJD.
	halley := orbit.Elements{
		Q:     0.5870992,
		E:     0.9671429,
		I:     162.26269,
		Omega: 58.42008,
		W:     111.33249,
		TpJD:  2446467.395,
	}
	halleyPos := orbit.ApparentGeocentricEquatorial(
		time.Date(1986, 2, 9, 0, 0, 0, 0, time.UTC),
		halley,
	)
	fmt.Printf("halley ra=%.6f dec=%.6f distance=%.6f\n", halleyPos.RA, halleyPos.Dec, halleyPos.Distance)
}

Output:

ceres ra=7.739532 dec=-10.625981 distance=2.164391
halley ra=312.112360 dec=-11.826451 distance=1.533936

Orbital elements have epochs. The farther the target date is from the epoch, the more static-element error can grow.

If the source provides long-term linear rates such as ADot/EDot/IDot/OmegaDot/WDot/MDot, they can be filled into Elements to reduce medium- and long-term drift.

Positions

Name Purpose Units and convention
EclipticPosition Ecliptic spherical return value Lon/Lat in degrees, Distance in AU
EquatorialPosition Equatorial spherical return value RA/Dec in degrees, Distance in AU
HeliocentricEclipticJ2000 Heliocentric J2000 mean ecliptic geometric, no light-time
HeliocentricEcliptic Heliocentric ecliptic of date geometric, referred to the mean equinox of date
GeocentricEclipticJ2000 Geocentric J2000 mean ecliptic geometric, Earth and target at the same instant
GeocentricEcliptic Geocentric ecliptic of date geometric, referred to the mean equinox of date
GeocentricEquatorialJ2000 Geocentric J2000 mean equatorial geometric, J2000 obliquity
GeocentricEquatorial Geocentric mean equatorial of date geometric, obliquity of date
AstrometricGeocentricEquatorialJ2000 Astrometric geocentric J2000 equatorial geometric position plus light-time, directly comparable with J2000 catalogues
ApparentGeocentricEcliptic Apparent geocentric ecliptic light-time plus nutation, without a full aberration model
ApparentGeocentricEquatorial Apparent geocentric equatorial light-time plus nutation, without a full aberration model
ApparentTopocentricEquatorial Apparent topocentric equatorial apparent geocentric plus topocentric parallax

Each step down the chain adds one correction to the same instant — geometric, then light-time, then nutation, then topocentric:

h := orbit.HeliocentricEcliptic(when, ceres)                  // heliocentric ecliptic of date, geometric
g := orbit.GeocentricEquatorialJ2000(when, ceres)             // geocentric J2000 mean equatorial
a := orbit.ApparentGeocentricEquatorial(when, ceres)          // apparent geocentric equatorial
t := orbit.ApparentTopocentricEquatorial(when, ceres, 121.4737, 31.2304, 20)
e := orbit.ApparentGeocentricEcliptic(when, ceres)
fmt.Printf("h=%.6f %.6f %.6f\n", h.Lon, h.Lat, h.Distance)
fmt.Printf("g=%.6f %.6f %.6f\n", g.RA, g.Dec, g.Distance)
fmt.Printf("a=%.6f %.6f t=%.6f %.6f e=%.6f\n", a.RA, a.Dec, t.RA, t.Dec, e.Lon)

The ...J2000 variants and the of-date variants are two different frames: the former stay in the J2000 mean ecliptic/equinox, which suits catalogue comparison and long-term archiving; the latter use the mean ecliptic/equinox of date, which suits "today's sky" expressions. Distance is the instantaneous distance for the geometric interfaces and the light-time-converged distance for Astrometric...; the two differ by roughly the displacement during the light-time.

Geometry

Name Purpose Units and convention
SunDistance Heliocentric distance AU, geometric
EarthDistance Geocentric distance AU, geometric
Elongation Solar elongation degrees, apparent geocentric angular separation
PhaseAngle Phase angle degrees, 0° means the fully lit face points at the observer
IlluminatedFraction Illuminated fraction dimensionless, typically within [0,1]
Phase Alias of the illuminated fraction identical to IlluminatedFraction
ParallacticAngle Parallactic (zenith-direction) angle degrees, hour angle and declination from one and the same topocentric solve

orbit also provides common observing geometry and lightweight photometry helpers:

r := orbit.SunDistance(when, ceres)                         // heliocentric distance
delta := orbit.EarthDistance(when, ceres)                   // geocentric distance
elong := orbit.Elongation(when, ceres)                      // solar elongation
phase := orbit.PhaseAngle(when, ceres)                      // phase angle
k := orbit.IlluminatedFraction(when, ceres)                 // illuminated fraction
mag := orbit.AsteroidMagnitudeHG(when, ceres, 3.34, 0.12)   // H-G asteroid magnitude
q := orbit.ParallacticAngle(when, ceres, 121.4737, 31.2304, 20) // parallactic angle from a site

fmt.Printf("r=%.6f delta=%.6f elong=%.6f phase=%.6f k=%.6f mag=%.3f q=%.6f\n",
	r, delta, elong, phase, k, mag, q)

Near 180° elongation the phase angle approaches 0° and IlluminatedFraction approaches 1; all three quantities come from the same geocentric geometry.

ParallacticAngle does not solve for declination separately: it reuses the same topocentric solve as HourAngle, so that hour angle and declination never come from two Julian-day paths about 1 ULP apart and introduce sub-nanodegree drift.

All geometry helpers depend only on the absolute instant of date; ParallacticAngle is the only one that also takes observer parameters.

Rise, Set, and Culmination

Name Purpose Units and convention
Altitude Apparent altitude degrees, apparent topocentric position on the observer's local civil day
Zenith Zenith distance degrees, equal to 90 - Altitude
Azimuth Apparent azimuth degrees, north 0°, increasing toward east
HourAngle Topocentric hour angle degrees
CulminationTime Culmination time time.Time, keeps the location of the input date
RiseTime Rise time (time.Time, error), second value is a sentinel error
SetTime Set time (time.Time, error), second value is a sentinel error
ERR_ORBIT_NEVER_RISE Sentinel: target never rises that day returned by RiseTime
ERR_ORBIT_NEVER_SET Sentinel: target never sets that day returned by SetTime
fmt.Println(orbit.Zenith(when, ceres, 121.4737, 31.2304, 20))
fmt.Println(orbit.HourAngle(when, ceres, 121.4737, 31.2304, 20))
fmt.Println(orbit.CulminationTime(when, ceres, 121.4737, 31.2304, 20).Format(time.RFC3339))

day := time.Date(2025, 11, 21, 0, 0, 0, 0, site)
set, err := orbit.SetTime(day, ceres, 121.4737, 31.2304, 20, true)
if errors.Is(err, orbit.ERR_ORBIT_NEVER_SET) {
	fmt.Println("never sets today", set)
}

Treat an orbit as an observable target for topocentric pointing:

site := time.FixedZone("CST", 8*3600)
when := time.Date(2025, 11, 21, 20, 0, 0, 0, site)

alt := orbit.Altitude(when, ceres, 121.4737, 31.2304, 20) // topocentric altitude
az := orbit.Azimuth(when, ceres, 121.4737, 31.2304, 20)   // topocentric azimuth
rise, _ := orbit.RiseTime(time.Date(2025, 11, 21, 0, 0, 0, 0, site), ceres, 121.4737, 31.2304, 20, true) // rise time

fmt.Printf("alt=%.6f az=%.6f rise=%s\n", alt, az, rise.Format(time.RFC3339))

These observing helpers work on topocentric apparent coordinates and suit rise/set and pointing support for asteroids, comets, or custom two-body targets.

With aero false the criterion is the geometric horizon; with aero true the target altitude is taken as -0.5667° plus the horizon dip derived from ellipsoidal height and latitude.

The second return value of RiseTime/SetTime is a real error: only a day without a rise/set is mapped to ERR_ORBIT_NEVER_RISE / ERR_ORBIT_NEVER_SET, and any other failure passes through unchanged.

Photometry

Name Purpose Units and convention
AsteroidMagnitudeHG Asteroid apparent magnitude, H-G model absoluteMagnitude is H and slopeParameter is G; both dimensionless
fmt.Printf("H-G magnitude=%.3f\n", orbit.AsteroidMagnitudeHG(when, ceres, 3.34, 0.12))
fmt.Printf("r=%.6f delta=%.6f elong=%.6f\n",
	orbit.SunDistance(when, ceres), orbit.EarthDistance(when, ceres), orbit.Elongation(when, ceres))
fmt.Printf("phase=%.6f k=%.6f k2=%.6f\n",
	orbit.PhaseAngle(when, ceres), orbit.IlluminatedFraction(when, ceres), orbit.Phase(when, ceres))

The H-G model uses only heliocentric distance, geocentric distance, and phase angle; it does not introduce the target radius, albedo, or rotation, and this package never fills in a default value for G — that comes from the external catalogue.

Visual Binaries

Name Purpose Units and convention
VisualBinaryElements Visual-binary orbital elements PeriodYears in mean solar years, PeriastronYear as a decimal year, SemiMajorAxis in arcseconds, Inclination/AscendingNode/PeriastronArgument in degrees, Eccentricity dimensionless
VisualBinaryPosition Computed visual-binary position MeanAnomaly/EccentricAnomaly/TrueAnomaly/PositionAngle in degrees, Radius/Separation in arcseconds
VisualBinary Visual-binary position at an instant converts the instant to a UTC decimal year, then applies the classical apparent-orbit formula
VisualBinaryByYear Visual-binary position by decimal year takes the decimal year directly, skipping the instant conversion
gammaVir := orbit.VisualBinaryElements{
	PeriodYears: 171.37, PeriastronYear: 1836.433, Eccentricity: 0.8808,
	SemiMajorAxis: 3.746, Inclination: 146.05, AscendingNode: 31.78, PeriastronArgument: 252.88,
}
vb := orbit.VisualBinaryByYear(2026.0, gammaVir)
fmt.Printf("theta=%.6f rho=%.6f M=%.6f\n", vb.PositionAngle, vb.Separation, vb.MeanAnomaly)

orbit also includes a lightweight visual-binary solver using the classical apparent-orbit formula from chapter 55 of Astronomical Algorithms:

gammaVir := orbit.VisualBinaryElements{
	PeriodYears:        171.37,
	PeriastronYear:     1836.433,
	Eccentricity:       0.8808,
	SemiMajorAxis:      3.746,
	Inclination:        146.05,
	AscendingNode:      31.78,
	PeriastronArgument: 252.88,
}
vb := orbit.VisualBinary(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), gammaVir)
fmt.Printf("theta=%.6f rho=%.6f\n", vb.PositionAngle, vb.Separation) // position angle and separation

Position angle is measured with north at 0° and east at 90°, and both the separation and the radius vector Radius are in arcseconds.

Usage examples

Picking a position layer

helio := orbit.HeliocentricEcliptic(when, ceres)
geo := orbit.GeocentricEclipticJ2000(when, ceres)
ast := orbit.AstrometricGeocentricEquatorialJ2000(when, ceres)
app := orbit.ApparentGeocentricEquatorial(when, ceres)
fmt.Println(helio.Lon, helio.Lat, helio.Distance)
fmt.Println(geo.Lon, geo.Lat, geo.Distance)
fmt.Println(ast.RA, ast.Dec)
fmt.Println(app.RA, app.Dec, app.Distance)
19.251489 -9.315340 2.912174
2.147652 -12.026350 2.262445
6.795211 -10.172420
7.125008 -10.028726 2.262489

The four layers mean different things: Heliocentric* is relative to the Sun (the first number is the heliocentric distance), Geocentric*J2000 is the J2000 geocentric position, Astrometric*J2000 removes light-time and suits catalog comparison, and Apparent* is the apparent position of the day used for observing and charts.

For a topocentric apparent position use ApparentTopocentricEquatorial(when, ceres, lon, lat, height).

Will it rise tonight, and how high is it now?

fmt.Println(orbit.RiseTime(when, ceres, 121.4737, 31.2304, 20, true))
fmt.Println(orbit.CulminationTime(when, ceres, 121.4737, 31.2304, 20))
fmt.Println(orbit.Altitude(when, ceres, 121.4737, 31.2304, 20),
	orbit.Azimuth(when, ceres, 121.4737, 31.2304, 20))
2025-11-21 14:41:48.913 CST <nil>
2025-11-21 20:19:34 CST
48.472628 172.699168
  • aero = true solves against the horizon corrected for refraction and apparent radius; height is ellipsoidal height in metres and longitude is east positive.
  • Circumpolar or polar targets have no rise or set: RiseTime/SetTime return the orbit.ERR_ORBIT_NEVER_RISE / ERR_ORBIT_NEVER_SET sentinels, so branch with errors.Is.

Distances, phase angle and H-G magnitude

fmt.Println(orbit.SunDistance(when, ceres), orbit.EarthDistance(when, ceres))
fmt.Println(orbit.Elongation(when, ceres), orbit.PhaseAngle(when, ceres))
fmt.Println(orbit.IlluminatedFraction(when, ceres), orbit.AsteroidMagnitudeHG(when, ceres, 3.34, 0.12))
2.912174 2.262445
122.264630 16.670089
0.978986 8.360
  • PhaseAngle is the Sun-target-Earth angle in degrees and IlluminatedFraction is the illuminated fraction; Phase is only an alias of IlluminatedFraction, so do not read it as a phase angle.
  • H-G magnitudes take the absolute magnitude H and the slope parameter G (Ceres uses 3.34 and 0.12 in the example).

Orbit types and position layers

parabolic := orbit.Elements{Q: 0.9, E: 1, I: 30, Omega: 40, W: 50, TpJD: 2461000.5}
hyperbolic := orbit.Elements{Q: 1.2, E: 1.05, I: 30, Omega: 40, W: 50, TpJD: 2461000.5}
fmt.Println(orbit.MeanMotion(parabolic), orbit.MeanAnomaly(when, parabolic))
fmt.Println(orbit.TrueAnomaly(when, parabolic), orbit.TrueAnomaly(when, hyperbolic))
fmt.Println(orbit.MeanMotion(ceres))
NaN NaN
0.817533 0.537610
0.21429712142765137
  • Parabolic and hyperbolic orbits only work in perihelion form (Q plus TpJD): mean motion and mean anomaly are undefined and return NaN, while the true anomaly solves for all three orbit types.
  • When cross-checking against an external ephemeris, align the position layer and frame first (Elements is always J2000 mean ecliptic/equinox); ADot…WDot only apply to the classical ellipse form, and a non-zero MDot replaces the default mean motion.

Parameter and result conventions

Position Layers

Layer Representative interfaces Corrections included
Heliocentric geometric HeliocentricEcliptic / HeliocentricEclipticJ2000 none
Geocentric geometric GeocentricEcliptic / GeocentricEclipticJ2000 / GeocentricEquatorial / GeocentricEquatorialJ2000 minus the heliocentric Earth position
Geocentric astrometric AstrometricGeocentricEquatorialJ2000 light-time
Geocentric apparent ApparentGeocentricEcliptic / ApparentGeocentricEquatorial light-time + nutation
Topocentric apparent ApparentTopocentricEquatorial and all rise/set helpers light-time + nutation + topocentric parallax

Units and Frame Conventions

  • Angles are always in degrees, distances in AU, ellipsoidal heights for topocentric and rise/set helpers in meters, and time as time.Time.

  • The frame of Elements is the J2000 mean ecliptic and mean equinox. Interfaces ending in ...J2000 keep that frame; the other HeliocentricEcliptic/GeocentricEcliptic/GeocentricEquatorial variants are of-date quantities, so do not mix the two frames.

  • Positions come in three layers: Heliocentric*/Geocentric* are geometric, AstrometricGeocentricEquatorialJ2000 adds light-time to the geometric position (solved iteratively in distance, at most 8 passes, converged at 1e-12 days), and Apparent* adds nutation on top of light-time.

    The planetary convention of this repository is exactly "light-time + nutation, without a full external aberration model", so Apparent* is at the same level as the planet packages and must not be treated as a full apparent place.

  • MeanMotion/MeanAnomaly/TrueAnomaly return degrees; mean motion and mean anomaly are undefined for parabolic and hyperbolic orbits.

Time Scale and Input Reading

  • EpochJD and TpJD are TT/TDB Julian days. The coordinate, geometry, and photometry interfaces treat date as an absolute instant (date.UTC(), then UTC2TT to TT/TDB).

    UTC2TT treats civil values as UT1 before 1972-01-01 and uses the built-in leap-second table inside the exact window; that table can be overridden with astro.SetTTMinusUTC.

  • Instantaneous topocentric quantities combine the local clock fields with date.Zone() to recover the absolute instant. UTC and local-zone representations of the same instant give the same position. Rise/set searches also use the local date, so choose the zone for the observing calendar.

  • CulminationTime, RiseTime, and SetTime keep the zone of the input date and return civil instants. Internally they step back 12 hours when date.Hour() > 12, so that the search anchor stays within the selected date.

  • Every public time.Time output is a civil value (UTC label); UT1 and TT only appear in internal conversions. For explicit conversion use the root package's astro.UT1FromUTC / astro.TTFromUTC.

Zero Values and Out-of-Range Input

  • The zero value of Elements is not a valid orbit. The classical elliptical form requires a finite positive A, E within [0,1), and finite EpochJD and M0.

    Parabolic and hyperbolic orbits with E >= 1 can only use the perihelion form, which requires a finite positive Q, a finite TpJD, E >= 0, and finite I/Omega/W.

  • When Q > 0 and TpJD is finite the perihelion form wins: A, M0, and EpochJD are ignored, ADot/EDot/IDot/OmegaDot/WDot have no effect, and only a non-zero MDot is used as the mean angular rate.

  • With invalid elements MeanMotion and MeanAnomaly return NaN and the positional interfaces return three NaNs.

    AsteroidMagnitudeHG returns NaN when its inputs are non-finite or when heliocentric distance, geocentric distance, or phase angle is non-positive, and +Inf when the H-G phase blend is zero.

  • RiseTime and SetTime return the sentinel errors ERR_ORBIT_NEVER_RISE / ERR_ORBIT_NEVER_SET when no rise/set exists on the supplied local day, and pass any other failure through; on success the second return value is nil.

  • VisualBinary and VisualBinaryByYear fill every numeric field of VisualBinaryPosition with NaN when PeriodYears <= 0, SemiMajorAxis <= 0, Eccentricity outside [0,1), or any element is non-finite.

Accuracy and Applicability

  • orbit is two-body conic propagation: it contains only the supplied elements, with no planetary perturbations, non-gravitational terms, or relativistic corrections. The farther the date from the epoch, the larger the static-element error; ADot…WDot only mitigate linear drift and cannot replace a re-fit or a numerical integration.
  • The difference between the Apparent* and topocentric quantities is purely geometric plus nutation, and neither includes atmospheric refraction; only aero=true in the rise/set helpers folds refraction into the criterion (target altitude -0.5667° plus the horizon dip derived from ellipsoidal height and latitude).
  • RiseTime/SetTime iterate the rise/set geometry in the nominal zone round(observerLon/15), so the site should sit near the center of its zone; only one root is solved, and boundary cases such as polar or circumpolar targets are reported through the sentinel errors.
  • The visual-binary solver uses the classical apparent-orbit formula from chapter 55 of Astronomical Algorithms and does not apply when Eccentricity >= 1; it is a geometric projection only, with no mass, photometry, or perturbation information.

Common Pitfalls

  • Treating a HeliocentricEcliptic result as geocentric: heliocentric quantities are centered on the Sun, while geocentric ones have already subtracted the heliocentric Earth position.
  • Treating Apparent* as a refraction-corrected apparent place: refraction appears only in the rise/set and observing helpers, not in the coordinate interfaces.
  • Probing with a zero-value Elements: mean motion and the positional interfaces quietly return NaN instead of reporting an error.
  • Expecting ADot…WDot to take effect in the perihelion form: those rates only propagate in the classical elliptical form.

To feed the ecliptic/equatorial coordinates here into a star catalogue or topocentric quantities, see star and coordinate tools.