A personal astronomy library developed over years for calendrical work, amateur observing, outreach demos, and lightweight research.
> This project is mainly for learning and validating astronomical algorithms. The results are intended for serious amateur use.
The implementation follows *Astronomical Algorithms* and provides calendar conversion, Sun/Moon/planet positions, eclipses, rise/set/transit times, lunar phases, stars, coordinate transforms, physical ephemerides, research formulas, and generic small-body orbit propagation. The Sun and planets use built-in VSOP87-style analytical terms, while the Moon uses a built-in ELP2000/82-style series. No external JPL ephemeris files are required.
Unless noted otherwise, coordinates are apparent-of-date coordinates. Angles are in degrees, apparent diameters and semidiameters are in arcseconds, and distances use the unit implied by the function name, usually `AU` or `km`.
## Contents
- [Install](#install)
- [Highlights](#highlights)
- [Package Overview](#package-overview)
- [Scope And Accuracy](#scope-and-accuracy)
- [Quick Start](#quick-start)
- [Calendar And Solar Terms](#calendar-and-solar-terms)
- 9100+ star catalog entries, constellation lookup, proper-motion propagation, rise/set, parallactic angle, and apparent altitude
- Coordinate transforms, topocentric coordinates, sidereal time, precession, nutation, angular distance, refraction, airmass, parallactic angle, and Galactic coordinates
- Standalone formulas for blackbody radiation, synodic periods, photometry, telescope limiting magnitude, stellar radius/temperature/luminosity relations, and airmass models
- Generic heliocentric two-body orbit propagation for asteroids, comets, dwarf planets, and hypothetical objects, including phase/photometry helpers and visual-binary position solving
- Apparent/mean solar time, solar hour angle, mean-time/zone-time hour-angle helpers, planar-sundial geometry, and equatorial/horizontal/vertical dial helpers
## Package Overview
| Package | What it provides |
| --- | --- |
| `calendar` | Gregorian/lunisolar conversion, solar terms, historical era names, old-calendar metadata |
| `coord` | Ecliptic/equatorial/horizontal transforms, sidereal time, precession, nutation, topocentric helpers, refraction, airmass, parallactic angle, Galactic coordinates, and research helpers with manual obliquity/hour angle |
| `sun` | Solar position, rise/set, twilight, equation of time, apparent solar time, apparent altitude, parallactic angle, diameter, solar `P/B0/L0` |
| `lite/sun` / `lite/moon` | Lightweight Sun/Moon approximation chains for minute-level rise/set, lightweight sky position, and lunar-phase work |
| `eclipse` / `eclipse/svg` | Global/local solar and lunar eclipses, solar central paths, partial footprints, local visibility filtering, Saros metadata, SVG output |
| `formula` | Date-independent astronomy and olympiad-style formulas, including pure airmass models |
| `orbit` | Generic heliocentric conic propagation for elliptical, near-parabolic, parabolic, and hyperbolic orbits, plus phase/photometry helpers and a lightweight visual-binary solver |
| `sundial` | Apparent/mean solar time, solar hour angle, mean-time/zone-time hour-angle helpers, planar geometry, time-line and declination-curve sampling, equatorial/horizontal/vertical dial helpers |
Many position APIs also provide `...N` variants:
-`n < 0`: use all built-in terms embedded in this repository
-`n >= 0`: truncate the series, useful for performance comparisons, rough estimates, or algorithm studies
"All built-in terms" means the table entries shipped inside this package. It does not mean the complete original external VSOP/ELP long tables.
Airmass API distinction:
-`coord.Airmass...` is the observing-oriented layer. It can start from apparent altitude directly, or first estimate refraction from true altitude and then compute airmass.
-`formula.Airmass...` is the raw formula layer. It does not apply refraction.
## Scope And Accuracy
### Sun and planets
The Sun and planets use built-in VSOP87-style analytical terms. The current embedded tables cover roughly 4000 years around J2000.
| Target | Longitude / latitude | Distance |
| --- | --- | --- |
| Sun / Earth | about `0.1"` | about `0.1 x 10^-6 AU` |
| Mercury, Venus | about `0.2"` | about `0.2 x 10^-6 AU` |
| Mars | about `0.5"` | about `1 x 10^-6 AU` |
| Jupiter | about `0.5"` | about `3 x 10^-6 AU` |
| Saturn | about `0.5"` | about `5 x 10^-6 AU` |
| Uranus | about `1"` | about `20 x 10^-6 AU` |
| Neptune | about `1"` | about `40 x 10^-6 AU` |
This is suitable for ordinary calendrical work, observing support, outreach, and personal research. For spacecraft navigation, occultation prediction, or strict dynamical integration, use JPL DE or another professional ephemeris.
### Moon
The Moon uses a built-in truncated ELP2000/82-style series retaining the major periodic terms. The package stays lightweight and does not require external ephemeris files. It is suitable for Chinese-calendar new moons, lunar phases, rise/set, lunar eclipses, and ordinary positional work. For lunar laser ranging, long-term physical libration, or professional occultation work, use JPL or a dedicated lunar ephemeris.
The four principal phases keep the historical pinyin names and also expose English aliases:
-`ShuoYue` / `NewMoon`
-`WangYue` / `FullMoon`
-`ShangXianYue` / `FirstQuarter`
-`XiaXianYue` / `LastQuarter`
The matching `Next*`, `Last*`, and `Closest*` helpers are available in both naming styles.
### Lite lightweight chains
`lite/sun` and `lite/moon` are independent approximation chains. They do not depend on the VSOP87 or ELP2000/82 engines used by `sun` / `moon`, and are intended for CPU- or memory-constrained environments.
-`lite/sun`: simplified true/apparent solar longitude formulas plus lightweight equatorial conversion
-`lite/moon`: Schlyter-style lunar approximation with about 15 perturbation terms plus lightweight topocentric correction
- rise/set search: fixed-step scanning plus bisection, without the high-precision nutation iteration used by the main chain
- zero heap allocation in the computation path; lunar position is about 1 microsecond and 20-60x faster than the main chain
| Package | Position model | Rise/set search | Main use |
| --- | --- | --- | --- |
| `lite/sun` | simplified true/apparent solar longitude plus lightweight equatorial conversion | `30 min` scan plus bisection | sunrise/sunset, solar altitude, watch faces, frontend refresh loops |
Use the main `sun` / `moon` chains for eclipses, physical libration, or high-latitude edge cases.
### Regression references
The following areas have been checked against JPL Horizons, NASA GSFC, IMCCE, and other public references:
- apparent diameters of the Sun, planets, and Moon
- solar physical ephemerides `P/B0/L0`
- planetary rise, transit, and set events
- Earth perihelion and aphelion
- Moon perigee and apogee
- maximum lunar declinations
- solar and lunar eclipses
- Galilean satellite events
The README examples are illustrative. The repository tests contain the exact baselines.
## Quick Start
### Calendar And Solar Terms
The `calendar` package converts between Gregorian dates and the traditional Chinese lunisolar calendar, and exposes solar terms. The supported range is from the first lunisolar month of 104 BCE through year 3000. The calendar is lunisolar in the strict sense, but public function names use `Lunar` for readability and convention.
For historical input, Chinese era names stay in Chinese. This is part of the API surface, because historical Chinese dates are normally written that way.
#### Data sources and checks
- **[-103, 1912]**: based on 《寿星天文历》 and corrected against the tables maintained by [Professor ytliu0](https://ytliu0.github.io/ChineseCalendar/index_simp.html). This range contains lunisolar data only, not historical solar-term records.
- **[1913, 3000]**: uses VSOP87 for apparent solar longitude and ELP2000 for new moons, following the modern Chinese-calendar rule set in GB/T 33661-2017. Solar terms and new moons use Beijing time (UTC+08:00).
- Solar terms before 1912 are computed with modern astronomical methods and may differ from historical almanac dates by 1-2 days.
#### Usage notes
1. A single Gregorian date may map to multiple lunisolar dates in periods with parallel regimes and calendars, such as the Three Kingdoms period.
2. A single lunisolar date may map to multiple Gregorian dates when a regime changed calendar rules. For example, during Wu Zetian's calendar reform, year 3 of Shengli had two twelfth months.
3. Gregorian handling is based on Julian Day:
- after `1582-10-15`: Gregorian calendar
- before `1582-10-04`: Julian calendar
- before year 8 CE: proleptic Julian calendar
- the day after `1582-10-04` is `1582-10-15`
- dates from `1582-10-05` through `1582-10-14` are invalid and rejected
- year `0` means 1 BCE, year `-1` means 2 BCE, and so on
Time zone note: standard wrappers such as `SolarToLunar` and `LunarToSolar` use Beijing time. Lower-level `Solar` and `Lunar` allow custom time zones for research under the modern Chinese-calendar algorithm.
Go-specific note: before `1582-10-15`, Go's `time.Time` uses the proleptic Gregorian calendar, so `time.Time.Weekday()` does not match this library's Julian/Gregorian handling. To get the weekday used here:
```go
// date should be the local midnight of the target day.
weekday := int(calendar.Date2JDE(date)+1.5) % 7
// 0 means Sunday, 1 means Monday, ..., 6 means Saturday.
```
#### Calendar conversion
```go
package main
import (
"encoding/json"
"fmt"
"time"
"b612.me/astro/calendar"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
// Example 1: Gregorian to lunisolar. This date is in the Three Kingdoms period, so multiple parallel-calendar results are returned.
date := time.Date(240, 1, 1, 8, 8, 8, 8, cst)
lunar, _ := calendar.SolarToLunar(date)
fmt.Println(lunar.LunarDescWithEmperor())
// Structured lunisolar information for each matching historical result.
info := lunar.LunarInfo()
data, _ := json.MarshalIndent(info, "", " ")
fmt.Println(string(data))
// Example 2: lunisolar to Gregorian by string. This is the date from Su Shi's 《记承天寺夜游》.
solar, _ := calendar.LunarToSolar("元丰六年十月十二日")
for _, v := range solar {
fmt.Println(v.Time())
fmt.Println(v.LunarDescWithEmperor())
}
// Example 3: lunisolar to Gregorian by numeric fields. 2026 month 1 day 1 is Chinese New Year.
2020-03-20 11:49:34.502393603 +0800 CST // March Equinox
2020-03-20 11:49:34.502393603 +0800 CST // same result from direct longitude input
```
### Sun And Moon
#### Observing-angle semantics
-`Altitude`: altitude angle; horizon is `0°`, zenith is `+90°`
-`Zenith`: zenith distance; zenith is `0°`, horizon is `90°`
- In older versions, `Zenith` incorrectly returned altitude. Current versions return zenith distance.
#### Sunrise/sunset and moonrise/moonset
Moon rise/set times are date-based. Rise and set events on the same civil day are not guaranteed to form one continuous observing cycle. For example, the Moon may set at 01:00 and rise again at noon; in that case, the evening moonset belongs to the following date's query.
```go
package main
import (
"fmt"
"time"
"b612.me/astro/moon"
"b612.me/astro/sun"
)
func main() {
// Xi'an, China. Longitude east and latitude north are positive; elevation is 0 m.
var lon, lat, height float64 = 108.93, 34.27, 0
cst := time.FixedZone("CST", 8*3600)
// All "today" semantics are based on this local civil date.
date := time.Date(2020, 1, 1, 8, 8, 8, 8, cst)
// Civil morning twilight begins when the Sun is 6 degrees below the horizon.
fmt.Println(sun.MorningTwilight(date, lon, lat, -6))
// Sunrise in Xi'an on this date, with atmospheric refraction.
fmt.Println(sun.RiseTime(date, lon, lat, height, true))
// Upper culmination of the Sun in Xi'an.
fmt.Println(sun.CulminationTime(date, lon))
// Sunset in Xi'an on this date, with atmospheric refraction.
fmt.Println(sun.SetTime(date, lon, lat, height, true))
// Civil evening twilight ends when the Sun is 6 degrees below the horizon.
fmt.Println(sun.EveningTwilight(date, lon, lat, -6))
// Moonrise in Xi'an on this date, with atmospheric refraction.
fmt.Println(moon.RiseTime(date, lon, lat, height, true))
// Upper culmination of the Moon in Xi'an.
fmt.Println(moon.CulminationTime(date, lon, lat))
// Moonset in Xi'an on this date, with atmospheric refraction.
fmt.Println(moon.SetTime(date, lon, lat, height, true))
fmt.Println(moon.AscendingNode(nodeDate), moon.DescendingNode(nodeDate)) // ascending-node and descending-node longitudes, degrees
```
Here:
-`AscendingNode`: ecliptic longitude where the Moon crosses from south of the ecliptic to north of it
-`DescendingNode`: ecliptic longitude where the Moon crosses from north of the ecliptic to south of it
- both values are degrees, and are usually about `180°` apart at the same instant
Output:
```text
340.9570862454423 160.95708624544227 // lunar ascending-node and descending-node longitudes, degrees
```
#### Lunar phases
```go
package main
import (
"fmt"
"time"
"b612.me/astro/moon"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
// Instant of observation.
date := time.Date(2020, 1, 1, 8, 8, 8, 8, cst)
// Illuminated fraction of the lunar disk.
fmt.Println(moon.Phase(date))
// Chinese textual phase description.
fmt.Println(moon.PhaseDesc(date))
// Next new moon; moon.NextNewMoon(date) is the English alias.
fmt.Println(moon.NextShuoYue(date))
// Next first quarter; moon.NextFirstQuarter(date) is the English alias.
fmt.Println(moon.NextShangXianYue(date))
// Next full moon; moon.NextFullMoon(date) is the English alias.
fmt.Println(moon.NextWangYue(date))
// Next last quarter; moon.NextLastQuarter(date) is the English alias.
fmt.Println(moon.NextXiaXianYue(date))
}
```
Output:
```text
0.3000437415436273 // about 30% of the lunar disk is illuminated
上峨眉月 // Chinese phase description
2020-01-25 05:41:55.820311009 +0800 CST // next new moon
2020-01-03 12:45:20.809730887 +0800 CST // next first quarter
2020-01-11 03:21:14.729664623 +0800 CST // next full moon
2020-01-17 20:58:20.955985486 +0800 CST // next last quarter
```
Phase aliases:
-`ShuoYue` / `NewMoon`
-`WangYue` / `FullMoon`
-`ShangXianYue` / `FirstQuarter`
-`XiaXianYue` / `LastQuarter`
The matching `Next*`, `Last*`, and `Closest*` functions are also available.
#### Lite Sun And Moon
`lite/sun` and `lite/moon` use the same calling style as the main chain. Error levels are listed in [Lite lightweight chains](#lite-lightweight-chains).
Solar-eclipse calculation lives in `eclipse`; SVG generation lives in `eclipse/svg`. The default lunar-radius convention follows NASA bulletin split-`k`. IAU single-`k` variants are available through same-named `...IAUSingleK` functions.
Common entry points:
-`SolarEclipseOnDate`: detect whether a global solar eclipse occurs near a local date
-`LastSolarEclipse` / `NextSolarEclipse` / `ClosestSolarEclipse`: search global solar eclipses
-`LocalSolarEclipseOnDate`: detect whether a site can see a local solar eclipse on that date
-`LastLocalSolarEclipse` / `NextLocalSolarEclipse` / `ClosestLocalSolarEclipse`: search locally visible solar eclipses
-`SolarEclipseCentralPath`: compute central line, northern/southern limits, and greatest-eclipse point
-`SolarEclipsePartialFootprints`: compute the partial-eclipse penumbral footprint on Earth
-`eclipse/svg.LocalSolarEclipseSVG`: render a local solar-disk SVG
`SolarEclipseInfo`, `LocalSolarEclipseInfo`, and the embedded `Eclipse` field in `SolarEclipsePath` / `SolarEclipsePartialFootprintsInfo` include Saros metadata:
-`HasSaros`: whether a Saros series was matched
-`Saros.Series`: NASA Saros series number
-`Saros.Member`: 1-based member number within that series
-`Saros.Count`: total member count of that series
Saros note:
- One Saros is about `6585.321` days, or `223` synodic months, commonly described as about `18 years 11 days 8 hours`.
- A Saros series is a sequence of eclipses separated by one Saros period. `Series` identifies the sequence, while `Member` / `Count` describe the event's position in it.
- Saros metadata belongs to the eclipse event, not to the observing site. Global, local, path, and footprint results for the same eclipse should report the same Saros.
- For example, the `2024-04-08` North American total solar eclipse is member `30/71` of Solar Saros `139`.
#### Timing checks against NASA material
Solar-eclipse timing is checked in two forms:
- **Global eclipses**: greatest-eclipse UT, magnitude, gamma, greatest-eclipse coordinates, and path width. Current regression samples include `2023-04-20`, `2024-04-08`, `2024-10-02`, and `2025-03-29`.
- **Local eclipses**: local first contact, greatest eclipse, last contact, and totality/annularity duration. Current samples include a Chicago partial eclipse, the 2024 total-eclipse greatest point, and the 2024 annular-eclipse greatest point.
| Check type | Sample | Time fields | Result |
| --- | --- | --- | --- |
| Global solar eclipse | 4 modern eclipses | Greatest-eclipse UT | second-level agreement, current samples are within an `8 s` threshold |
| Local solar eclipse | 3 observing sites | greatest eclipse, first contact, last contact | NASA local-circumstance public values are often rounded to whole minutes; current results match those rounded minute values |
| Local central eclipse | 2 central-eclipse points | totality/annularity duration | second-level agreement, current samples are within a `5 s` threshold |
Global eclipse references often publish seconds, so second-level checks are meaningful there. Many local-circumstance pages publish contact times only to whole minutes, so minute-level agreement is the correct interpretation for those fields.
#### 2009 Yangtze River total eclipse near Yangshan
`2009-07-22` is the well-known Yangtze River total eclipse. The example below uses a site near Yangshan at the Yangtze River estuary southeast of Shanghai, close to the center line. At greatest eclipse the Sun and Moon centers are very close, and totality lasts about 5 minutes 57 seconds.
```go
package main
import (
"fmt"
"time"
"b612.me/astro/eclipse"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
date := time.Date(2009, 7, 22, 12, 0, 0, 0, cst)
// Near Yangshan, Shanghai. East longitude and north latitude are positive; elevation is 0 m.
info, ok := eclipse.LocalSolarEclipseOnDate(date, 121.9850, 30.6167, 0)
fmt.Println(ok, info.Type) // whether a local eclipse is found; eclipse type
fmt.Println(info.HasSaros, info.Saros) // Saros match flag; series, member number, total count
2009-07-22 11:03:13.974365293 +0800 CST // last contact
5m56.632827222s // totality duration
magnitude=1.076997 obscuration=1.000000 altitude=57.292 // magnitude, obscuration, solar altitude at greatest eclipse
greatest lon=144.1177 lat=24.2193 width=258.3km center=268 // global greatest point, path width, center-line sample count
```
#### 2012 Xiamen annular eclipse
The `2012-05-21` annular eclipse was visible from the southeast coast of China. The Xiamen example has the Sun about 9.6 degrees above the horizon at greatest eclipse, and annularity lasts about 4 minutes 19 seconds.
```go
package main
import (
"fmt"
"time"
"b612.me/astro/eclipse"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
date := time.Date(2012, 5, 21, 12, 0, 0, 0, cst)
info, ok := eclipse.LocalSolarEclipseOnDate(date, 118.0894, 24.4798, 0)
fmt.Println(ok, info.Type) // whether a local eclipse is found; eclipse type
fmt.Println(info.HasSaros, info.Saros) // Saros match flag; series, member number, total count
2012-05-21 07:20:55.029697716 +0800 CST // last contact
4m19.19316262s // annularity duration
magnitude=0.933290 obscuration=0.872480 altitude=9.567 // magnitude, obscuration, solar altitude at greatest eclipse
```
#### Solar-eclipse SVG
The modern city example uses the `2035-09-02` total solar eclipse in Beijing. With approximate downtown coordinates (`116.4074E`, `39.9042N`), this event belongs to Solar Saros `145` as member `23/77`, and local totality lasts about `1m33s`.
The default solar-eclipse SVG header includes Saros metadata and totality/annularity duration. `LocalSolarEclipseSVGOptions` can override:
-`Title`: main title
-`SummaryText` / `GreatestText` / `MetaText`: three subtitle lines under the title
`Saros` has the same meaning as in the solar-eclipse section:
-`Saros.Series`: NASA lunar Saros series number
-`Saros.Member`: 1-based member number within that series
-`Saros.Count`: total member count of that series
For example, the cross-year total lunar eclipse on `2028-12-31 / 2029-01-01` is member `49/72` of Lunar Saros `125`.
Two shadow-radius conventions are retained:
- **Danjon**, default and recommended: multiplies only the lunar horizontal-parallax term by `1.01`, then combines it with the solar semidiameter and solar parallax. NASA GSFC's current lunar-eclipse catalogs and diagram pages use this style.
- **Chauvenet**, compatibility convention: starts with `0.99834 x Earth equatorial radius` and then multiplies the full shadow radii by `51/50`. This is closer to older traditional tables and is useful for compatibility checks.
Differences:
-`Chauvenet` gives larger penumbral and umbral shadows. Penumbral magnitude is usually about `0.025` larger, and umbral magnitude about `0.005` larger.
- For edge cases, `Chauvenet` can push an eclipse toward a deeper type.
- Use the default `Danjon` APIs for NASA catalogs, modern ephemeris software, and current public eclipse material.
- Use explicit `Chauvenet` functions only for old-baseline compatibility.
#### Code example
```go
package main
import (
"fmt"
"time"
"b612.me/astro/eclipse"
)
func main() {
date := time.Date(2029, 1, 1, 0, 0, 0, 0, time.UTC)
// Default Danjon model, closer to NASA current material.
info := eclipse.ClosestLunarEclipse(date)
fmt.Println(info.Type) // eclipse type
fmt.Println(info.HasSaros, info.Saros) // Saros match flag; series, member number, total count
fmt.Println(info.Maximum) // greatest-eclipse time
fmt.Println(info.PenumbralMagnitude, info.UmbralMagnitude) // penumbral and umbral magnitudes
| 2026-03-03 total lunar eclipse | Danjon | -0.000072053 | -0.000065148 | second-level agreement, max error 6.380 s |
| 2026-03-03 total lunar eclipse | Chauvenet | +0.025594905 | +0.004939948 | compatibility model, not the NASA timing baseline |
| 2026-08-28 partial lunar eclipse | Danjon | -0.000118545 | -0.000028773 | second-level agreement, max error 6.179 s |
| 2026-08-28 partial lunar eclipse | Chauvenet | +0.025562714 | +0.004962282 | compatibility model, not the NASA timing baseline |
| 2024-03-25 penumbral lunar eclipse | Danjon | -0.000181657 | see note below | second-level agreement, max error 7.781 s |
| 2024-03-25 penumbral lunar eclipse | Chauvenet | +0.026039769 | see note below | compatibility model, not the NASA timing baseline |
For the `2026-03-03` total lunar eclipse, current default `Danjon` differences against NASA are:
- type: both `total`
- penumbral magnitude: `2.183727947` vs NASA `2.1838`, error `-0.000072053`
- umbral magnitude: `1.150634852` vs NASA `1.1507`, error `-0.000065148`
- P1 error: `+3.400 s`
- U1 error: `+5.801 s`
- U2 error: `+6.261 s`
- greatest eclipse error: `+5.897 s`
- U3 error: `+5.776 s`
- U4 error: `+6.328 s`
- P4 error: `+6.380 s`
For pure penumbral eclipses, NASA may publish negative `umbral magnitude`, meaning the Moon's disk center remains outside the umbral boundary by that amount. This library preserves that negative value, so pure penumbral cases are compared in the same convention.
#### Lunar-eclipse SVG
The default lunar-eclipse SVG header includes Saros metadata. `LunarEclipseSVGOptions` can override:
-`Title`: main title
-`SummaryText` / `MaximumText` / `CoordinatesText` / `DurationText` / `MetaText`: five information lines under the title
-`ContactsTitle`: contact-time section title
-`DirectionText` / `FooterNote`: footer direction note and extra note
```go
package main
import (
"fmt"
"os"
"time"
eclipsesvg "b612.me/astro/eclipse/svg"
)
func main() {
// Render the shadow-path diagram for the cross-year total lunar eclipse on 2029-01-01 UTC.
Mercury and Venus also expose `NextTransit` / `LastTransit` / `ClosestTransit` for geocentric planetary transits. "Geocentric" means the planet disk crosses the solar disk as seen from Earth's center; it does not test whether the Sun is above the horizon at a particular observing site. For observing plans, combine this with local solar altitude and weather.
```go
package main
import (
"fmt"
"time"
"b612.me/astro/mercury"
"b612.me/astro/venus"
)
func main() {
// Next geocentric Mercury transit after the beginning of 2019.
fmt.Println(mars.RiseTime(date, lon, lat, height, true))
fmt.Println(mars.SetTime(date, lon, lat, height, true))
// Current apparent magnitude of Mars.
fmt.Println(mars.ApparentMagnitude(date))
// Earth-Mars distance.
fmt.Println(mars.EarthDistance(date))
// Sun-Mars distance.
fmt.Println(mars.SunDistance(date))
}
```
Output:
```text
2020-10-14 07:25:47.740884125 +0800 CST // next opposition of Mars
2021-01-29 09:39:30.916356146 +0800 CST // next conjunction of Jupiter
2019-04-30 10:28:27.453395426 +0800 CST // previous Saturn station from prograde to retrograde
saturn B=23.577026 Bp=23.266930 P=6.629811 dU=1.171017 major=34.133852 minor=13.652911 // Saturn ring B, B', P, dU, major axis, minor axis
2021-01-14 21:35:01.269377768 +0800 CST // next Uranus station from retrograde to prograde
2019-12-08 17:00:13.772284984 +0800 CST // previous eastern quadrature of Neptune
2020-06-07 03:10:57.179121673 +0800 CST // next western quadrature of Mars
2020-01-01 04:40:05.409269034 +0800 CST <nil> // Mars rise time in Xi'an; no error
2020-01-01 14:56:57.175483703 +0800 CST <nil> // Mars set time in Xi'an; no error
1.57 // Mars apparent magnitude
2.1820316323604088 // Earth-Mars distance, AU
1.5894169865107062 // Sun-Mars distance, AU
```
`saturn.Ring` returns `RingInfo`: `EarthLatitude` is ring opening angle B, `SunLatitude` is B', `PositionAngle` is the position angle of the northern semiminor axis, `DeltaU` is the Saturnicentric longitude difference between the Sun and Earth in the ring plane, and `MajorAxis` / `MinorAxis` are the apparent outer major/minor axes in arcseconds.
#### Planetary physical ephemerides
All seven major planets provide `Physical` / `PhysicalN` for disk orientation, sub-Earth/sub-Sun coordinates, and north-pole position angle. Jupiter additionally exposes System I/II/III central meridians, and Saturn exposes ring parameters.
```go
package main
import (
"fmt"
"time"
"b612.me/astro/jupiter"
"b612.me/astro/saturn"
)
func main() {
date := time.Date(2025, 11, 1, 0, 0, 0, 0, time.UTC)
// Jupiter: DS and DE are planetocentric declinations of the Sun and Earth relative to Jupiter's equator.
// CMI/CMII/CMIII are Jupiter System I/II/III central meridians, degrees.
-`GalileanPhenomenonEvent` treats the satellite as a point and checks when its center enters or leaves Jupiter's disk. It is suitable for fast phenomenon search and internal state checks.
-`GalileanPhenomenonContactEvent` includes the finite disk of the satellite and splits disappearance and reappearance contact windows. It is the better match for IMCCE tables such as `TR.D/TR.F/OC.D/OC.F/EC.D/EC.F/SH.D/SH.F`.
The two conventions may differ by up to about 7 minutes in duration. This is a definition difference, not a timing-accuracy failure. Use `GalileanPhenomenonContactEvent` for observing predictions and direct comparison with public almanacs.
```go
package main
import (
"fmt"
"time"
"b612.me/astro/jupiter"
)
func main() {
date := time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC)
// Instantaneous positions of the four satellites relative to Jupiter's center.
io x=-0.658543 y=-0.035608 front=true // Io X/Y offset from Jupiter center, in Jupiter radii; in front of Jupiter
europa ra=110.769323 dec=22.335800 // Europa apparent RA and Dec, degrees
io transit=true occultation=false eclipse=false shadow=true // Io is transiting, and its shadow is also transiting
europa transit=false occultation=false eclipse=false shadow=false // Europa has no transit, occultation, eclipse, or shadow transit at this instant
event valid=true sat=1 type=transit // next valid event is an Io transit
2026-01-16 16:32:47.785289883 +0000 UTC // Io transit begins
2026-01-16 17:40:43.882995843 +0000 UTC // midpoint of the Io transit
2026-01-16 18:48:40.519664883 +0000 UTC // Io transit ends
2h15m52.734375s // Io transit duration
contact valid=true sat=2 type=occultation // next valid contact event is a Europa occultation
2026-01-17 01:00:34.99533087 +0000 UTC // Europa occultation disappearance starts
2026-01-17 01:02:31.714070141 +0000 UTC // model center crossing during disappearance
2026-01-17 01:04:28.432809412 +0000 UTC // disappearance ends
2026-01-17 02:27:37.807798683 +0000 UTC // deepest occultation
2026-01-17 03:50:48.120300471 +0000 UTC // reappearance starts
2026-01-17 03:52:43.901527225 +0000 UTC // model center crossing during reappearance
2026-01-17 03:54:39.68275398 +0000 UTC // reappearance ends
```
External baseline summary:
-`Satellites` positions relative to Jupiter center: maximum sample difference against JPL Horizons about `X=0.252"`, `Y=0.108"`.
-`SatellitePhenomena` shadow-transit shadow-center offsets: maximum sample difference against JPL Horizons about `X=0.051"`, `Y=0.016"`; boolean phenomenon flags match in the samples.
-`GalileanPhenomenonContactEvent` against IMCCE 2026 tables: maximum contact-time difference in current samples about `72 s`; maximum contact-duration difference about `17 s`.
-`GalileanPhenomenonEvent` is not the IMCCE D/F contact convention. Direct comparison of its start/end times with IMCCE contact tables can differ by up to about `7 min` because the event definitions are different.
### Stars
The built-in star database contains 9100+ stars and supports proper-motion propagation.
2019-12-31 19:22:56.176710426 +0800 CST // rise time of Sirius
2020-01-01 05:30:39.834894239 +0800 CST // set time of Sirius
Canis Major // English constellation containing Sirius
5h58m10.19s // right ascension of Vega in year 13600
84°19′26.25″ // declination of Vega in year 13600
天狼 Sirius -1.46 // first brightest-star entry: Chinese name, common English name, apparent magnitude
```
### Coordinate Tools
`coord` is the user-facing coordinate wrapper. Unless noted otherwise, angles are degrees, sidereal time is in hours, and `time.Time` is treated as an absolute instant and internally converted to UTC.
143.99353431082105 18.7404068044953 // topocentric RA and Dec
manual az=281.869347 alt=24.489608 zen=65.510392 ha=73.866900 // manual-LST horizontal result and hour angle
gal lon=0.000047 lat=-0.000079 // Galactic longitude and latitude
apparent alt=10.092644 // apparent altitude after refraction estimate
```
Research-style `coord` helpers do not automatically substitute the current obliquity or sidereal time. They are useful for experiments with custom axial tilts or manually specified hour angles. For ordinary observing calculations, use the `time.Time` based APIs such as `EclipticToEquatorial` and `EquatorialToHorizontal`.
Observing helpers:
-`ParallacticAngle` / `ParallacticAngleByHourAngle`: parallactic angle, or the direction angle of the zenith at the target
-`Airmass...FromApparentAltitude`: apply empirical airmass formula directly when apparent altitude is already known
-`Airmass...FromTrueAltitude`: estimate refraction from pressure/temperature, convert true altitude to apparent altitude, then compute airmass
```go
// Parallactic angle of the target, useful for camera rotation, spectrograph slit direction, and field orientation.
// With true altitude as input, estimate refraction first and then compute empirical airmass.
x := coord.AirmassKastenYoungFromTrueAltitude(10, 1010, 0)
fmt.Printf("q=%.6f airmass=%.6f\n", q, x)
```
The same observing helpers are also exposed in `sun`, `moon`, `star`, and the seven major-planet packages. If apparent altitude is already available and only the raw formula is needed, use `formula.Airmass...`.
### Formula Helpers
`formula` contains common formulas that do not depend on a specific date or ephemeris. They are useful for estimates, teaching, and lightweight research.
```go
package main
import (
"fmt"
"b612.me/astro/formula"
)
func main() {
// Empirical limiting magnitude for a 70 mm refractor at a site with naked-eye limit 6.
-`AirmassKastenYoung` / `AirmassPickering`: apparent-altitude input, no automatic refraction correction
```go
fmt.Println(formula.AirmassPlaneParallel(30)) // plane-parallel airmass from true altitude
fmt.Println(formula.AirmassKastenYoung(5)) // Kasten-Young airmass from apparent altitude
fmt.Println(formula.AirmassPickering(5)) // Pickering airmass from apparent altitude
fmt.Println(formula.AirmassPlaneParallelByZenithDistance(60)) // plane-parallel airmass from zenith distance
```
### Generic Small-Body Orbits
`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.
ceres ra=7.739532 dec=-10.625981 distance=2.164391 // apparent geocentric RA, Dec, and distance of Ceres
halley ra=312.112360 dec=-11.826451 distance=1.533936 // apparent geocentric RA, Dec, and distance of Halley's Comet
```
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.
Common observing geometry and lightweight photometry helpers:
```go
r := orbit.SunDistance(when, ceres) // heliocentric distance
fmt.Printf("theta=%.6f rho=%.6f\n", vb.PositionAngle, vb.Separation) // position angle and separation
```
### Sundial And Apparent Solar Time
`sundial` groups apparent solar time, solar hour angle, and the geometry needed to draw sundials. It uses the same underlying `sun` APIs and does not introduce a separate solar algorithm.
-`TrueSolarTime`: local apparent solar time at the specified longitude
-`MeanSolarTime`: local mean solar time at the specified longitude
-`HourAngle`: signed solar hour angle, negative before noon and positive after noon
-`MeanSolarHourAngle` / `ZoneTimeHourAngle`: convert local mean solar time or zone-clock time to apparent solar hour angle
-`PlanarDial` / `Geometry` / `ShadowPointByHourAngleDeclination`: general geometric core for a planar sundial
-`PlaneIlluminatedHourAngleIntervals` / `IlluminatedHourAngleIntervals`: analytic hour-angle intervals for plane illumination and usable sunlight
-`DeclinationCurve` / `DeclinationCurveAt`: segmented sundial curve samples by declination or date
-`MeanSolarTimePoint` / `ZoneTimePoint` / `MeanSolarTimeLine` / `ZoneTimeLine`: attach mean-time or zone-time lines directly to the dial geometry
-`EquatorialNorthDial` / `EquatorialSouthDial` / `HorizontalDial` / `VerticalDial`: special cases for equatorial, horizontal, and vertical dials
-`HorizontalHourLineAngle`: hour-line angle on a horizontal sundial for a given latitude and hour angle
-`HorizontalHourLineAngleAt`: current horizontal hour-line angle from date, longitude, and latitude
Notes:
-`date` passed to `MeanSolarTimePoint` / `MeanSolarTimeLine` should be in the target site's local mean-solar-time zone. The most direct source is `MeanSolarTime(...)`.
-`ZoneTimePoint` / `ZoneTimeLine` ignore the original clock fields in `date`; they use only the calendar date and time zone, then replace the clock reading with `zoneTimeHours`.
## Implemented
- Sun position, altitude, zenith distance, azimuth, culmination, twilight, rise/set, solar terms, solar eclipses, solar physical ephemerides