Observatory-grade astronomy and observation-planning engine for Go.
Scale-aware time arithmetic · SOFA-rigorous coordinate transforms · sub-minute rise/set accuracy · production scheduling · validated against USNO, JPL Horizons, and NASA Eclipse Catalogs.
Fifteen lines: where's a star right now, and when does it rise, transit, and set from your backyard?
package main
import (
"fmt"
"github.com/TuSKan/astrogo/angle"
"github.com/TuSKan/astrogo/coord"
"github.com/TuSKan/astrogo/plan"
"github.com/TuSKan/astrogo/time"
)
func main() {
loc, _ := coord.NewEarthLocation(-23.5505, -46.6333, 760) // São Paulo
site, _ := plan.NewSite("Backyard", loc)
sirius := plan.NewStar("Sirius", angle.Hour(6.7525), angle.Deg(-16.7161))
tonight := time.Date(2026, 4, 15, 22, 0, 0, 0, time.LocationUTC)
eval, _ := plan.IsObservable(sirius, tonight, site, plan.Altitude{Threshold: angle.Deg(30)})
events, _ := plan.VisibilityEvents(tonight, tonight.AddDays(1), sirius, site)
fmt.Printf("Sirius right now: altitude %.1f°, observable above 30°: %v\n",
eval.AltAz.Alt().Degrees(), eval.Observable)
for _, e := range events {
fmt.Printf(" %-8s %s\n", e.Kind, e.Time.Format("2006-01-02 15:04 MST"))
}
}Sirius right now: altitude 64.8°, observable above 30°: true
Set 2026-04-16 02:49 UTC
Rise 2026-04-16 13:40 UTC
Transit 2026-04-16 20:12 UTC
No API keys, no downloads, no Python underneath for this example — every number above came from SOFA-derived algorithms running in pure Go. (Higher-precision JPL ephemerides are available too, opt-in — see Data downloads & offline usage.) Every code sample in this README is copy-pasted from a program that was actually compiled and run; none of it is aspirational.
Existing astronomy tools are powerful, but often:
- tightly coupled to Python
- difficult to optimize for high-throughput workloads
- not designed for Go's type system and performance model
astrogo aims to bring:
- Astropy-level capabilities
- Astroplan-style observation workflows
- Go-level performance and control
Designed from the ground up for Go: no dynamic magic, no hidden global state, zero-allocation hot paths.
The best way to see whether a library's numbers are trustworthy is to point it at a question with a real, checkable answer. These are full write-ups — narrative analysis backed by runnable code and tables you can verify against published references.
| Showcase | The question | Code |
|---|---|---|
| When Did Jesus Die? | "The sky keeps receipts." Three historical dating puzzles — the Star of Bethlehem, the ministry's start, the Crucifixion — resolved with eclipses, conjunctions, and lunar crescent visibility instead of manuscripts. | examples/10_jesus_christ/ |
| The Great Planet Parade | On Feb 28 2025, all seven planets were above the horizon at once from São Paulo. Was that actually visible, and how rare is it? | examples/16_planet_parade/ |
| Equinox & Solstice Almanac | A decade of seasons, eclipses, and apsides computed from first principles — no lookup tables, no curve fits, just JPL DE442 and root-finding. | examples/17_equinox_prediction/ |
| Moonlit Sky Brightness | How much does a full moon actually degrade your limiting magnitude, and by how many degrees of separation does that recover? | examples/18_sky_brightness/ |
| Satellite Tracking | Predict ISS passes over your location from live NORAD/CelestTrak data — AOS, max elevation, LOS, ground track. | examples/12_satellite_tracking/ |
| What's Visible Tonight | What can I actually see in the sky tonight brighter than magnitude X — stars, deep-sky objects, planets, the Moon, even asteroids and comets, all in one query? | examples/20_whats_visible_tonight/ |
go get github.com/TuSKan/astrogoResolve real catalog targets, check tonight's twilight and Moon, score observability, and produce an optimized schedule for the whole night.
package main
import (
"context"
"fmt"
"log"
"github.com/TuSKan/astrogo/angle"
"github.com/TuSKan/astrogo/catalog"
"github.com/TuSKan/astrogo/coord"
"github.com/TuSKan/astrogo/ephemeris"
"github.com/TuSKan/astrogo/plan"
"github.com/TuSKan/astrogo/time"
)
const layout = "2006-01-02 15:04 MST"
func main() {
// ── Observer Setup: São Paulo ──
loc, _ := coord.NewEarthLocation(-23.5505, -46.6333, 760)
site, _ := plan.NewSite("São Paulo", loc)
// ── Night boundaries ──
eph := ephemeris.Default()
tonight := time.Date(2026, 4, 15, 22, 0, 0, 0, time.LocationUTC)
tomorrow := tonight.AddDays(1)
sunrise, sunset, _ := plan.SunriseSunset(tonight, tomorrow, site, eph)
fmt.Printf("Sunset: %s\n", sunset.Time.Format(layout))
fmt.Printf("Sunrise: %s\n", sunrise.Time.Format(layout))
dawn, dusk, _ := plan.AstronomicalDawnDusk(tonight, tomorrow, site, eph)
fmt.Printf("Astro dusk: %s → Astro dawn: %s\n", dusk.Time.Format(layout), dawn.Time.Format(layout))
// ── Moon phase check ──
nextFull, _ := plan.NextFullMoon(tonight, eph)
fmt.Printf("Next Full Moon: %s\n", nextFull.Time.Format(layout))
frac, _, _ := plan.MoonIllumination(tonight, eph)
fmt.Printf("Moon illumination: %.0f%%\n", frac*100)
// ── Targets ──
// Resolve by name — coordinates come from SIMBAD/OpenNGC automatically.
resolver := catalog.NewResolver(catalog.SIMBAD, catalog.OpenNGC)
ctx := context.Background()
omegaCenCat, err := resolver.Resolve(ctx, "NGC 5139") // Omega Centauri
if err != nil {
log.Fatal(err)
}
omegaCen := plan.FromCatalog(omegaCenCat, nil)
sgrACat, err := resolver.Resolve(ctx, "Sgr A*")
if err != nil {
log.Fatal(err)
}
sgrA := plan.FromCatalog(sgrACat, nil)
// Planets use ephemeris-backed constructors directly.
mars := plan.NewMars(eph)
// ── Observability + Scoring ──
constraints := []plan.Constraint{
plan.Altitude{Threshold: angle.Deg(30)},
plan.Airmass{Threshold: 2.0},
}
fmt.Println("\n── Observability ──────────────────────")
for _, obj := range []plan.Observable{omegaCen, sgrA, mars} {
eval, _ := plan.IsObservable(obj, tonight, site, constraints...)
score, _ := plan.ScoreObservable(obj, tonight, site, nil, nil, constraints...)
fmt.Printf(" %-18s Observable: %-5v Score: %5.1f\n",
obj.Name(), eval.Observable, score)
}
// ── Schedule the night ──
planner, _ := plan.NewPlanner(site, nil)
blocks := []*plan.Block{
{ID: "OmCen", Target: omegaCen, Duration: 45 * time.Minute, Priority: 3},
{ID: "SgrA", Target: sgrA, Duration: 60 * time.Minute, Priority: 5},
{ID: "Mars", Target: mars, Duration: 20 * time.Minute, Priority: 2},
}
strategy := &plan.SwapOptimizedStrategy{
Base: &plan.PriorityStrategy{},
MaxPasses: 5,
}
window := plan.Window{Start: dusk.Time, End: dawn.Time}
tm := &plan.BasicTransitionModel{BaseSetup: 5 * time.Minute}
schedule, _ := strategy.Schedule(planner, window, blocks, tm)
fmt.Println("\n── Schedule ──────────────────────────")
for _, sb := range schedule.Blocks {
fmt.Printf(" %s: %s → %s (score: %.1f)\n",
sb.Block.ID, sb.Window.Start.Format(layout), sb.Window.End.Format(layout), sb.Score)
}
for _, ub := range schedule.Unscheduled {
fmt.Printf(" [skip] %s: %s\n", ub.Block.ID, ub.Reason)
}
}Sunset: 2026-04-16 20:55 UTC
Sunrise: 2026-04-16 09:17 UTC
Astro dusk: 2026-04-15 22:07 UTC → Astro dawn: 2026-04-16 08:05 UTC
Next Full Moon: 2026-05-01 17:23 UTC
Moon illumination: 3%
── Observability ──────────────────────
HD 116586 Observable: false Score: 0.0
Sgr A* Observable: false Score: 0.0
Mars Observable: false Score: 0.0
── Schedule ──────────────────────────
SgrA: 2026-04-15 22:12 UTC → 2026-04-15 23:12 UTC (score: 45.0)
Mars: 2026-04-15 23:12 UTC → 2026-04-15 23:32 UTC (score: 29.0)
OmCen: 2026-04-15 23:32 UTC → 2026-04-16 00:17 UTC (score: 41.4)
Two notes on that output: the individual Observable checks are evaluated at exactly tonight (22:00 UTC), which is still twilight in São Paulo — that's expected, not a bug. The scheduler's own window search (dusk → dawn) is what actually finds when each target clears the constraints, which is why it successfully places all three blocks with real scores. And since resolver.Resolve hits live SIMBAD, the exact target name it returns for NGC 5139 (here HD 116586, one of its many catalog aliases) and the last decimal of the score can vary slightly between runs — everything else is deterministic.
More examples — batch transforms, moon phases & eclipses, lunar crescent, planetary geometry, satellite tracking
// Create one Context per epoch — amortizes the 91 µs SOFA Apco13 cost.
loc, _ := coord.NewEarthLocation(-23.55, -46.63, 760) // São Paulo
atm := atmosphere.AtAltitude(760) // SOFA refraction at all altitudes
ctx := coord.NewContext(time.NowUTC(), loc, atm)
// Transform 100 catalog stars for ~325 ns each (instead of ~91 µs each).
for _, star := range catalogStars {
altaz, _ := ctx.ICRSToAltAz(star.ICRS)
if altaz.Alt().Degrees() > 30 {
observable = append(observable, star)
}
}eph := ephemeris.Default()
start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.LocationUTC)
end := start.AddDays(365)
// All lunar phases for 2026
phases, _ := plan.MoonPhases(start, end, eph)
for _, p := range phases {
fmt.Printf("%s: %s\n", p.Phase, p.Time.Format(layout))
}
// Lunar eclipses — filtered by Danjon ecliptic latitude limit
eclipses, _ := plan.LunarEclipses(start, end, eph)
for _, e := range eclipses {
fmt.Printf("Lunar Eclipse: %s (γ=%.2f, lat=%.2f°)\n",
e.Time.Format(layout), e.Gamma, e.EclipticLatitude.Degrees())
}
// Next Full Moon from a reference date
nextFull, _ := plan.NextFullMoon(start, eph)
fmt.Printf("Next Full Moon: %s (illumination: %.0f%%)\n",
nextFull.Time.Format(layout), nextFull.Value*100)Full Moon: 2026-01-03 10:03 UTC
Last Quarter: 2026-01-10 15:49 UTC
New Moon: 2026-01-18 19:52 UTC
First Quarter: 2026-01-26 04:47 UTC
Lunar Eclipse: 2026-03-03 11:34 UTC (γ=0.23, lat=-0.36°)
Lunar Eclipse: 2026-08-28 04:13 UTC (γ=0.29, lat=0.46°)
Next Full Moon: 2026-01-03 10:03 UTC (illumination: 100%)
// Compute topocentric parameters for the evening of sighting
params, _ := plan.NewCrescentParams(sunsetTime, site.Location(), eph)
// Evaluate all 20 historical criteria simultaneously
result := params.EvaluateAll()
// Multi-zone classifications (Yallop, Odeh, Qureshi)
fmt.Printf("Yallop (1998): %s\n", result.Yallop.Label)
fmt.Printf("Odeh (2004): %s\n", result.Odeh.Label)
// Singular physical limits
fmt.Printf("Above Danjon limit: %v\n", result.Danjon)
fmt.Printf("MABIMS (2021): %v\n", result.MABIMS2021)Yallop (1998): Visible under perfect conditions
Odeh (2004): Visible to naked eye
Above Danjon limit: true
MABIMS (2021): true
eph := ephemeris.Default()
venus := plan.NewVenus(eph)
sun := plan.NewSun(eph)
// Greatest elongations of Venus over a full synodic period
elongations, _ := plan.GreatestElongations(start, end, venus, sun)
for _, e := range elongations {
fmt.Printf("%s: %.1f° at %s\n", e.Kind, e.Value, e.Time.Format(layout))
}
// Mars-Jupiter conjunctions
mars := plan.NewMars(eph)
jupiter := plan.NewJupiter(eph)
conj, _ := plan.Conjunctions(start, end, mars, jupiter)
for _, c := range conj {
fmt.Printf("Mars-Jupiter conjunction: %s\n", c.Time.Format(layout))
}
// Appulses (closest visual approach)
appulses, _ := plan.Appulses(start, end, mars, jupiter)
for _, a := range appulses {
fmt.Printf("Appulse: %s (min sep: %.2f°)\n", a.Time.Format(layout), a.Value)
}Greatest Elongation East: 45.9° at 2026-08-15 06:14 UTC
Greatest Elongation West: 47.0° at 2027-01-03 17:43 UTC
Mars-Jupiter conjunction: 2026-11-15 02:16 UTC
Appulse: 2026-11-16 01:38 UTC (min sep: 1.19°)
// Resolve ISS from the live NORAD/CelestTrak catalog.
resolver := catalog.NewResolver(catalog.NORAD)
target, _ := resolver.Resolve(context.Background(), "ISS (Zarya)")
// SGP4 provider — implements the same Provider interface as JPL planets.
prov, _ := ephemeris.NewProvider(ephemeris.Satellites, target.Name,
ephemeris.WithTLE(target.TLELine1, target.TLELine2))
defer prov.Close()
epoch := target.Epoch
alt, _ := ephemeris.Altitude(prov, 0, epoch)
fmt.Printf("Orbital altitude: %.0f km\n", alt)
// Topocentric look angle — same coord.Context API used for planets and stars.
observer, _ := coord.NewEarthLocation(-23.5505, -46.6333, 760)
ctx := coord.NewContext(epoch, observer, atmosphere.Atmosphere{})
altaz, _ := plan.LookAngle(prov, 0, ctx)
fmt.Printf("Az: %.1f° El: %.1f° Range: %.0f km\n",
altaz.Az().Degrees(), altaz.Alt().Degrees(), altaz.Dist())
// Predict passes over the observer (next 24 hours, min 10° elevation).
passes, _ := plan.SatellitePasses(prov, target.Name, epoch, epoch.AddDays(1), observer, angle.Deg(10))
for _, pass := range passes {
fmt.Printf("AOS: %s Max El: %.1f° LOS: %s Duration: %s\n",
pass.Rise.Time.Format("15:04:05 MST"), pass.Culmination.Elevation.Degrees(),
pass.Set.Time.Format("15:04:05 MST"), pass.Duration.Round(time.Second))
}Orbital altitude: 428 km
Az: 264.1° El: -50.1° Range: 10320 km
AOS: 09:56:17 UTC Max El: 20.3° LOS: 10:01:25 UTC Duration: 5m8s
AOS: 11:33:00 UTC Max El: 19.6° LOS: 11:38:05 UTC Duration: 5m6s
AOS: 19:45:03 UTC Max El: 73.1° LOS: 19:51:47 UTC Duration: 6m44s
(Live output — ISS orbital elements and pass geometry change day to day.)
| Time | Full UTC↔TAI↔TT↔TDB↔UT1 graph, Fairhead & Bretagnon TDB (±3 µs), explicit IERS UT1 error propagation |
| Coordinates | ICRS/Galactic/Ecliptic/AltAz/Geodetic, full Geometric→Astrometric→Apparent→Observed pipeline, Context caching (91 µs → 325 ns/transform) |
| Atmosphere | SOFA-rigorous refraction by default at all altitudes, Pickering (2002) airmass down to 0°, pluggable RefractionModel |
| Ephemerides | Sun/Moon/planets (SOFA), multi-kernel JPL SPK with on-demand Horizons fetching, SGP4 satellite propagation |
| Magnitude | Planets (Mallama & Hilton 2018), asteroids (HG/HG1G2/sHG1G2), comets, satellites, stars — validated 100% within 0.025 mag against the FINK/ZTF production pipeline |
| Catalogs | Unified resolve.Provider over SIMBAD, MAST, Gaia, VizieR, JPL Horizons & SBDB, OpenNGC, NORAD, FINK — streaming iter.Seq2, Arrow caching, retry/backoff |
| FITS & WCS | Image/BinTable/ASCII HDUs, gzip streams, mmap, TAN projection, Arrow export |
| Sky Brightness | Moonlight (Krisciunas & Schaefer 1991), zodiacal light (Leinert 1998), airglow, light-pollution floor — folds into observability scoring |
| Planning | Sub-second Chandrupatla/Brent boundary refinement, constraint-based scoring, Greedy/Priority/SwapOptimized scheduling strategies |
| Events | Rise/Set/Transit, Moon Phases, Seasons, Apsides, Eclipses, Conjunctions, Elongations, Satellite Passes, 20 historical lunar-crescent criteria |
Full feature list (by package)
- Angles (radians, degrees, sexagesimal — HMS/DMS parsing)
- Units and quantities
- Scale-aware time system (JD-based, full
UTC↔TAI↔TT↔TDB↔UT1conversion graph)- Fairhead & Bretagnon (1990) TDB correction (±3 µs residual)
- Cross-scale comparisons auto-unify via TT (2 ns same-scale fast path)
UT1()returns(Time, error)— explicit IERS data unavailability
- ICRS
- Galactic
- Ecliptic
- Horizontal (Alt/Az)
- Geodesic
- Full mapping: Geometric ↔ Astrometric ↔ Apparent ↔ Observed
- Frame-to-frame (Galactic, Ecliptic, ICRS, CIRS)
- Dynamic DUT1 tracking and Polar Motion (XP/YP) caching via IERS EOP rapid data
- One-time log warning when IERS data is unavailable (UT1 ≈ UTC fallback)
- Aberration, light deflection, proper motion, parallax handled natively
- SOFA-rigorous refraction by default at all altitudes (ICAO standard atmosphere)
- Pluggable
RefractionModelinterface with bidirectional refraction RefractionNone— bypass refractionRefractionApproximate— Saemundsson/Bennett tangent formula (~12 ns/call)RefractionRigorous— full pressure/temperature/humidity/wavelength correction (~14 ns/call)- Pickering (2002) airmass — stable down to 0° altitude (overcomes Kasten & Young limitations)
- Chromatic atmospheric dispersion via
Reducer.Disperse()
- Geodetic locations (WGS84) with nil-location guards
- Epsilon-tolerant site equality (1e-12 rad)
- Defensive catalog pointer copying
- Stateful
Contextcaching for batch transforms (73× speedup for 100-star batches)
- Sun and Moon positions
- Planetary positions (Mercury → Neptune)
- SGP4 satellite propagation — TEME→GCRS conversion, sub-satellite ground track, topocentric look angles
- High-performance JPL SPK provider:
- Multi-kernel architecture (load planets and small-bodies simultaneously)
- On-demand asteroid/comet fetching via JPL Horizons API
- Support for SPK Type 21 (Extended Modified Difference Arrays)
- Precedence-aware segment indexing (~85× faster lookups)
- Planets: Mallama & Hilton (2018) — Mercury through Neptune, Saturn ring correction, Neptune secular brightening
- Asteroids: H,G · H,G₁,G₂ · H,G₁₂* · sHG1G2 (Carry et al. 2024) — 7-parameter spin-geometry model
- Comets: IAU standard M₁/k₁ (total) + M₂/k₂ (nuclear)
- Satellites: McCants/Molczan sphere/cylinder phase functions
- Stars: Bouguer atmospheric extinction with altitude scaling, Gaia G→V/B transformations
- Sun/Moon: Distance modulus + Allen (2000) phase polynomial
- Validated against FINK/ZTF phunk pipeline — 100% match at 0.025 mag (186 r-band observations)
- Unified
resolve.Providerinterfaces (ObjectResolver,ConeSearcher,BrightObjectSearcher— bulk-list every object a provider knows brighter than a magnitude bound; implemented by SIMBAD, OpenNGC, and SBDB) - Hardware-optimized native caching via Apache Arrow columnar batches
- Modern Go 1.23 streaming
iter.Seq2iteration for memory-safe big data fetching - Resilient network layers with exponential backoff retry
- Production-grade bindings:
- SIMBAD (ADQL TAP)
- MAST (STScI CAOM Dual-Encoding support)
- JPL SBDB (Small-Body Database Search)
- Gaia & VizieR (Data TAP)
- OpenNGC (NGC/IC deep-sky catalog, fetched and cached on first use)
- NORAD/CelestTrak (GP data — OMM/JSON format aligned with Space Data Standards)
- FINK/ZTF SSOFT (sHG1G2 phase-curve parameters for ~95k asteroids — single-object JSON + bulk parquet)
- Read standard FITS files (Image, BinTable, ASCII Table HDUs)
- Gzip-compressed streams (
.fits.gz), memory-mapped access (OpenMmap) - Apache Arrow columnar export for catalog-scale table HDUs
- WCS — pixel-to-sky mapping with TAN (Gnomonic) projection and
ExtractWCSheader parser
plan.VisibleTonight— "what's visible in the sky tonight brighter than magnitude X", across stars, deep-sky objects, planets, the Moon, asteroids, and comets in one call, each annotated with its constellation and extinction-adjusted apparent magnitude;plan.WithPlanetaryMoons()opts into the 21 major moons of Mars/Jupiter/Saturn/Uranus/Neptune/Pluto too (off by default — their SPK kernels run ~64 MB–1.1 GB each)- Sub-second boundary refinement — Chandrupatla (continuous altitude) + bisection (discrete constraints)
- Observable windows with constraint evaluation
- Altitude/airmass/separation constraints
- Target scoring and ranking (
ScoreObservableat midpoint altitude × priority) - Production Scheduling Engine:
BlockandConfigurationabstractions for observing requestsTransitionModelfor slew and instrument setup time- Pluggable
Strategyallocators:GreedyStrategy— fast, linear scalingPriorityStrategy— priority-sorted greedySwapOptimizedStrategy— local search with adjacent swaps + gap insertion (monotonic improvement)
- Linear scaling benchmarked to 100 blocks
- Night-sky surface brightness decomposed into additive components, summed in linear flux space and converted to V mag/arcsec² only at the boundary:
Moonlight— scattered moonlight, Krisciunas & Schaefer (1991) closed formZodiacalLight— Leinert et al. (1998) Table 17, bilinear interpolationAirglow— dark-sky floor (Noll et al. 2012 / Patat 2008)Floor— light-pollution baseline from scalar SQM, directionalSQMGrid, orFloorFromBortle
skybrightness/atlas— offline, pure-Go artificial-brightness atlas providers (Falchi et al. 2016 World Atlas GeoTIFF, VIIRS-DNB)skybrightness/lpmap— live client for the lightpollutionmap.info QueryRaster API, with retry/backoff on transient failuresplan.LimitingMagnitudeConstraint/ScoreObservableSky— folds sky-brightness-derived limiting magnitude into observability constraints and scoring
- Unified
Solver— Chandrupatla root-finding (1997) + Brent's minimization - Moon Phases: New, First Quarter, Full, Last Quarter — ≤1 min vs USNO
- Moon Phase Events:
NextNewMoon,NextFullMoon,MoonPhasesviaEventFamilyIllumination - Earth's Seasons: Equinoxes and Solstices — 2–4 min vs USNO
- Visibility Events: Rise/Set ≤0.6 min vs USNO, Transit ≤0.5 min — 41/41 edge cases passing (polar, equatorial, 8849m altitude)
- Satellite Passes: AOS/TCA/LOS prediction with Chandrupatla-refined rise/set boundaries (
SatellitePasses) - Relational Geometry: Conjunction (RA), Conjunction (Ecliptic Longitude), Appulse, Opposition, Greatest Elongation, Quadrature
- Eclipse Detection:
LunarEclipses,SolarEclipsesvia ecliptic latitude filter (Danjon limit) - Convenience:
SunriseSunset,CivilDawnDusk,VisibilityEvents,Conjunctions,ConjunctionsEcliptic,Appulses,Oppositions,GreatestElongations
- 20 Historical Criteria (1910–2021) — Fotheringham, Danjon, Yallop, Odeh, Caldwell, MABIMS, and more
- Evaluates topocentric parameters (Altitude/Azimuth, Elongation, ArcV/Width, Lag Time)
EvaluateAllfor batch assessment across all 20 models simultaneously
astrogo follows a layered design:
flowchart TD
%% High-level Orchestration
plan[plan]
catalog[catalog]
fitsplan["fits/plan"]
%% Scientific Engines
ephemeris[ephemeris]
coord[coord]
atmosphere[atmosphere]
fits[fits]
skybrightness[skybrightness]
%% Data Providers
lpmap["skybrightness/lpmap"]
%% Primitive Foundation
subgraph Primitives
direction LR
time[time]
angle[angle]
vector[vector]
unit[unit]
constants[constants]
end
%% Dependency mappings (Top-Down: A imports B)
plan --> coord
plan --> ephemeris
plan --> catalog
plan --> atmosphere
plan --> skybrightness
fitsplan --> fits
fitsplan --> plan
catalog --> coord
catalog --> time
catalog --> angle
fits --> coord
ephemeris --> time
ephemeris --> vector
ephemeris --> coord
satellite["ephemeris/satellite"] --> ephemeris
satellite --> norad
norad["catalog/norad"] --> catalog
plan --> satellite
skybrightness --> angle
lpmap --> skybrightness
coord --> atmosphere
coord --> time
coord --> vector
coord --> angle
atmosphere --> angle
style Primitives fill:transparent,stroke:#888,stroke-dasharray: 5 5
- No cyclic dependencies: Clean unidirectional imports.
- Explicit data models: Structures over magic mappings.
- Separation of concerns: Domain physics (
atmosphere) decoupled from coordinate geometry (coord). - Batch-friendly computation paths:
Contextcaches expensive SOFA matrices once per epoch.
| Package | Purpose | Status |
|---|---|---|
remote |
Centralized endpoint registry, HTTP client (retry/backoff), consent-gated downloads, configurable data storage | ✅ Stable |
constants |
Universal and astronomical constants | ✅ Stable |
angle |
Angular types, HMS/DMS parsing | ✅ Stable |
vector |
3D geometry primitives | ✅ Stable |
time |
Astronomical time scales (JD-based, UTC/TAI/TT/TDB/UT1), Earth Orientation Parameters (DUT1, polar motion), epoch arithmetic (MJD, GAST, Julian epoch year, day-of-year) | ✅ Stable |
atmosphere |
Refraction models, airmass, dispersion | ✅ Stable |
coord |
Coordinate types, transforms, topocentric reduction | ✅ Stable |
ephemeris |
Solar system ephemerides (SOFA + JPL SPK) | ✅ Stable |
ephemeris/satellite |
SGP4 propagation, TEME→GCRS, look angles, ground track | ✅ Stable |
catalog/resolve |
Provider interface, HTTP client, Arrow cache | ✅ Stable |
catalog/{simbad,mast,gaia,sbdb,openngc,norad,fink} |
Fully-implemented catalog providers | ✅ Stable |
catalog/vizier |
ConeSearch against any registered VizieR table (2MASS, Hipparcos, Gaia DR3; extensible via tables.go) |
✅ Stable |
catalog/jpl |
Horizons name resolution — ambiguous major/small-body match tables and unambiguous exact matches | ✅ Stable |
magnitude |
Apparent magnitude (planets, asteroids, comets, satellites, stars) | ✅ Stable |
constellation |
IAU constellation lookup from an ICRS position (official 1930 boundaries) | ✅ Stable |
fits |
FITS I/O, WCS (TAN projection), mmap, Arrow export | ✅ Stable |
fits/plan |
FITS↔plan bridge (SiteFromFITS, TargetFromFITS) |
✅ Stable |
plan |
Observability, constraints, events, scheduling, satellite passes | ✅ Stable |
skybrightness, skybrightness/atlas |
Sky brightness model (moonlight, zodiacal light, airglow, light-pollution floor) | ✅ Stable |
skybrightness/lpmap |
Live lightpollutionmap.info client | ✅ Stable |
unit |
Physical unit and quantity system | ✅ Stable |
See VALIDATION.md for scientific validation status, USNO.md for the U.S. Naval Observatory accuracy report (41/41 tests passing, ≤0.6 min rise/set accuracy across 3 continents + polar/equatorial/8849m edge cases), and the FINK/ZTF sHG1G2 validation (100% match at 0.025 mag against the phunk production pipeline).
astrogo never downloads a file without your explicit consent — this is a deliberate,
enforced default, not a suggestion. Every external connection the library can make is
enumerated in the remote package's endpoint registry; nothing else
happens.
| Data | Endpoint | Typical size | When |
|---|---|---|---|
| JPL planetary kernel (de440s) | remote.NAIFSPK |
~32 MB | eph.NewProvider(eph.Planets/SmallBody/..., "de440s") |
| JPL planetary kernel (de440, de442) | remote.NAIFSPK |
~115 MB | eph.NewProvider(eph.Planets, "de440"/"de442") |
| JPL planetary kernel (de441 parts) | remote.NAIFSPK |
multi-GB each | eph.NewProvider(eph.Planets, "de441_part-1", eph.WithKernel("de441_part-2")) |
| Leap-second kernel (naif0012.tls) | remote.NAIFLSK |
~5 KB | always, alongside any JPL kernel |
| Small-body SPK (Horizons-generated) | remote.JPLHorizons |
KB–few MB | eph.NewProvider(eph.SmallBody, "433", ...) |
| Planetary satellite SPK (Io, Titan, Triton, ...) | remote.NAIFSPK |
~64 MB (Mars) – ~1.1 GB (Jupiter), ~2.4 GB for all 6 kernels | eph.NewProvider(eph.Moons, "sat441"), or plan.VisibleTonight(..., plan.WithPlanetaryMoons()) |
| IERS Earth-orientation data | remote.IERSFinals2000A |
~3.7 MB | automatic on first Time.EOP()/.UTC()/.UT1() query needing it |
| OpenNGC catalog CSVs | remote.OpenNGC |
~2 MB combined | catalog.NewResolver(catalog.OpenNGC, ...) |
For an accuracy/offline tradeoff comparison across ephemeris.Default() and the
JPL kernels above, see the ephemeris package doc's
"Choosing a Provider" section.
Both IERS and OpenNGC skip the download entirely when the upstream content hasn't changed since the last fetch — a HEAD-request content check (ETag/Content-Length) against what was cached, not a wall-clock expiration window, since the two sources mutate on their own schedules rather than yours.
Catalog resolvers (catalog/simbad, catalog/gaia, catalog/vizier, catalog/mast,
catalog/sbdb, catalog/norad, catalog/fink) and skybrightness/lpmap make small
request/response API calls, not bulk downloads — those are gated by endpoint
enable/disable and offline mode, not by download-size consent (the network call itself
is the explicit purpose of the method you're calling).
Nothing above ever downloads silently. Construct a JPL ephemeris provider with a cold cache and no consent granted, and you get an explicit, actionable error instead of a multi-hundred-MB surprise:
_, err := eph.NewProvider(eph.Planets, "de442")
// err: jpl: SPK kernel planets/de442.bsp: remote: download denied: planets/de442.bsp
// (size unknown from https://naif.jpl.nasa.gov/...); astrogo never downloads without
// consent — call remote.EnableDownloads(remote.NAIFSPK, maxSize) or pre-seed the fileGrant consent per endpoint, with an optional size cap (0 = unlimited):
import "github.com/TuSKan/astrogo/remote"
remote.EnableDownloads(remote.NAIFSPK, 200<<20) // allow up to 200 MB
remote.EnableDownloads(remote.NAIFLSK, 0) // the ~5 KB leap-second kernel, unlimited
p, err := eph.NewProvider(eph.Planets, "de442") // now downloads (once) and cachescatalog.OpenNGC follows the same pattern — enabling the endpoint is the only step required;
catalog.NewResolver's first use of it fetches and caches the catalog automatically:
remote.EnableDownloads(remote.OpenNGC, 5<<20) // ~2 MB combined source CSVs
resolver := catalog.NewResolver(catalog.OpenNGC, catalog.SIMBAD) // fetches OpenNGC on first useTo grant consent for every download-gated endpoint at once instead of enumerating them
individually, use remote.EnableAllDownloads (and its counterpart DisableAllDownloads):
remote.EnableAllDownloads(200 << 20) // NAIFSPK, NAIFLSK, IERSFinals2000A, OpenNGC — all at onceFor total control, install a custom policy instead of per-endpoint limits:
remote.SetPolicy(func(ep remote.Endpoint, size int64) error {
if size > 500<<20 {
return fmt.Errorf("refusing a %d-byte download of %s", size, ep.URL)
}
return nil // allow
})All downloaded/cached data — JPL kernels, the IERS runtime cache — lives under one
configurable base directory, default os.UserCacheDir()/astrogo (~/.cache/astrogo on
Linux, %LocalAppData%\astrogo on Windows, ~/Library/Caches/astrogo on macOS):
remote.SetDataDirPath("/data/astrogo-cache") // or remote.SetDataDir for a github.com/ungerik/go-fs FilePre-seed remote.DataDir() with the files you need (e.g. copy a kernel to
remote.DataDir()/jpl/planets/de442.bsp), then cut network access entirely:
remote.SetOffline(true)
p, err := eph.NewProvider(eph.Planets, "de442") // finds the pre-seeded kernel, zero networkEvery downloader checks the filesystem before the network, so a pre-seeded deployment
never dials out even without SetOffline — remote is the only thing that resolves or
opens these files, there is no separate local-only constructor to bypass it with.
IERS EOP data follows the same rule: drop finals2000A.data at
remote.DataDir()/iers/finals2000A.data and the first Time.EOP()/.UTC()/.UT1()
call finds it automatically — no explicit loader call needed.
remote.Endpoints() // inspect every endpoint astrogo can reach
remote.Disable(remote.SIMBAD) // block one endpoint (ErrEndpointDisabled)
remote.SetURL(remote.SIMBAD, "https://mirror.example/tap") // point at a mirror/proxy
remote.SetOffline(true) // global kill switch, all network accessNo package in astrogo embeds data at build time. IERS EOP data is obtained exclusively
at runtime, automatically and lazily the first time it's needed: a pre-seeded
finals2000A file on disk, then (consent-gated via remote.EnableDownloads) a network
fetch — there is no iers/data/ directory or go:embed to populate before building.
OpenNGC works the same way — like every other catalog provider, it fetches over the
network via
remote.EnableDownloads(remote.OpenNGC, ...) (see "Enabling a download" above).
astrogo uses github.com/hebl/gofa as a backend for standards-based astronomical algorithms (derived from SOFA).
These are wrapped internally to ensure:
- Clean public APIs
- Flexibility for future backends
- Isolation of low-level numerical details
Important
astrogo is pre-1.0 (currently v0.5.0) — the public API may still change. This release rebuilds the remote package's public API around the Endpoint struct (GetFile/CacheDir/NewClientFor replace the previous primitives; see CHANGELOG.md for the full breaking-change list). catalog/jpl and catalog/vizier — the two providers that previously gated a v1.0.0 API-stability commitment — are fully implemented (see Implementation Status above); see docs/ROADMAP.md for what's left before v1.0.0.
Warning
These are documented trade-offs, not bugs. They represent deliberate scope boundaries as of v0.5.0.
The SOFA Apco13 matrix computation in coord.NewContext costs ~91 µs. The scheduling hot path creates one Context per time step (shared across all constraints via the ConstraintCtx interface), rather than one per constraint per step.
Built-in constraints (Altitude, Airmass) implement ConstraintCtx automatically. Custom constraints that implement this interface will also benefit from the cached Context in the scheduler.
For batch transforms outside the scheduler, create one Context per epoch and reuse it:
ctx := coord.NewContext(epoch, site.Location(), site.Atmosphere())
for _, star := range targets {
altaz, _ := ctx.ICRSToAltAz(star.ICRS) // ~325 ns, not ~91 µs
}SwapOptimizedStrategy is a local search heuristic, not a global optimizer. It improves on greedy/priority strategies via adjacent swaps and gap insertion, with monotonic score guarantees — but it does not find the globally optimal schedule.
This is the same trade-off made by production observatory schedulers (ESO/VLT SCHED, STARS, Gemini) where tractability and predictability are preferred over the NP-hard combinatorial optimum. Users who need provable global optimality (integer linear programming, branch-and-bound, etc.) can implement the Strategy interface directly — it's a small, focused contract.
Sub-arcsecond topocentric accuracy and sub-second UT1 timing require IERS EOP data (finals2000A). Without it:
| Metric | With EOP | Without EOP |
|---|---|---|
| UT1 accuracy | <50 ms | ~0.9 s (UT1 ≈ UTC fallback) |
| Topocentric alt/az | <0.01″ | ~1″ |
| Rise/set timing | ≤0.6 min vs USNO | ≤0.7 min vs USNO |
The library logs a one-time warning when EOP data is unavailable (users who redirect or suppress logs won't see it — call time.Coverage() to check proactively). IERS data loads automatically and lazily the first time it's needed — a pre-seeded snapshot on disk, then a consent-gated network fetch — see Data downloads & offline usage.
The Fairhead & Bretagnon (1990) single-term TDB−TT correction has a ±3 µs residual. This is sufficient for observatory planning and even millisecond-precision pulsar timing. It is not sufficient for:
- Deep-space probe telemetry (needs full JPL DE-based TDB)
- Sub-microsecond timing array work
Full ephemeris-based TDB would add ~500× overhead per call. This trade-off is documented in time/time.go.
We actively track our development pipeline across multiple capability tiers focusing on High-Performance Vectorization, Scheduling Engines, and external Data Ecosystem integration.
Please see our full Project Roadmap to understand current milestones, tracking priorities, and architectural expansion goals.
Contributions welcome! See Contributing Guide and Code of Conduct.
MIT