Skip to main content

Python API Reference

While starward is primarily a CLI tool, all functionality is available as a Python library. This guide covers the programmatic API.


Installation

pip install starward

Or for development:

pip install -e ".[dev]"

Quick Start

from starward.core.time import JulianDate, jd_now
from starward.core.coords import ICRSCoord, GalacticCoord
from starward.core.angles import Angle, angular_separation
from starward.core.constants import AstronomicalConstants

# Current Julian Date
jd = jd_now()
print(f"JD: {jd.jd}, GMST: {jd.gmst():.4f}h")

# Parse coordinates
coord = ICRSCoord.from_string("12h30m +45d")
print(f"RA: {coord.ra.degrees}°, Dec: {coord.dec.degrees}°")

# Transform to Galactic
gal = coord.to_galactic()
print(f"l: {gal.l.degrees}°, b: {gal.b.degrees}°")

# Angular separation
c1 = ICRSCoord.from_string("10h +30d")
c2 = ICRSCoord.from_string("11h +31d")
sep = angular_separation(c1.ra, c1.dec, c2.ra, c2.dec)
print(f"Separation: {sep.degrees}°")

# Constants
const = AstronomicalConstants()
print(f"Speed of light: {const.c.value} {const.c.unit}")

starward.core.angles

Angle Class

Immutable angle representation with automatic unit conversion.

Creating Angles

from starward.core.angles import Angle

# From various units (use exactly ONE keyword argument)
a1 = Angle(degrees=45.5)
a2 = Angle(radians=0.7854)
a3 = Angle(hours=3.0333)
a4 = Angle(arcminutes=2730)
a5 = Angle(arcseconds=163800)

# From components
a6 = Angle.from_dms(45, 30, 0) # 45° 30' 00"
a7 = Angle.from_dms(-45, 30, 15.5) # -45° 30' 15.5"
a8 = Angle.from_hms(12, 30, 45) # 12h 30m 45s

# From string
a9 = Angle.parse("45d30m15s")
a10 = Angle.parse("12h30m45s")
a11 = Angle.parse("45:30:15")
a12 = Angle.parse("45.5")

Accessing Values

a = Angle(degrees=45.5)

# Different units
a.degrees # 45.5
a.radians # 0.7941...
a.hours # 3.0333...
a.arcminutes # 2730.0
a.arcseconds # 163800.0

Converting to Components

a = Angle(degrees=45.504306)

# DMS components
d, m, s = a.to_dms_components() # (45, 30, 15.5)

# HMS components
h, m, s = a.to_hms_components() # (3, 2, 1.03...)

Formatting

a = Angle(degrees=45.504306)

a.to_dms() # "45° 30′ 15.50″"
a.to_dms(precision=0) # "45° 30′ 16″"
a.to_hms() # "03ʰ 02ᵐ 01.03ˢ"
str(a) # "45.504306°"

Arithmetic

a = Angle(degrees=45)
b = Angle(degrees=30)

a + b # Angle(degrees=75)
a - b # Angle(degrees=15)
a * 2 # Angle(degrees=90)
a / 3 # Angle(degrees=15)
-a # Angle(degrees=-45)
abs(a) # Angle(degrees=45)

Comparison

a = Angle(degrees=45)
b = Angle(degrees=30)

a > b # True
a >= b # True
a < b # False
a == b # False
a != b # True

Trigonometry

a = Angle(degrees=30)

a.sin() # 0.5
a.cos() # 0.866...
a.tan() # 0.577...

Normalization

a = Angle(degrees=370)

a.normalize() # Angle(degrees=10) — centered on 180, range [0, 360)
a.normalize(center=0) # Angle(degrees=10) — range [-180, 180)
a.normalize(center=180) # Angle(degrees=10) — range [0, 360)

angular_separation()

Calculate the angular separation between two points using the Vincenty formula.

from starward.core.angles import Angle, angular_separation

ra1 = Angle(hours=10)
dec1 = Angle(degrees=30)
ra2 = Angle(hours=11)
dec2 = Angle(degrees=31)

sep = angular_separation(ra1, dec1, ra2, dec2)
print(f"Separation: {sep.to_dms()}")

# With verbose output
from starward.verbose import VerboseContext
ctx = VerboseContext()
sep = angular_separation(ra1, dec1, ra2, dec2, verbose=ctx)
ctx.print_steps()

Signature:

def angular_separation(
ra1: Angle, dec1: Angle,
ra2: Angle, dec2: Angle,
verbose: Optional[VerboseContext] = None
) -> Angle

position_angle()

Calculate the position angle from point 1 to point 2.

from starward.core.angles import Angle, position_angle

ra1 = Angle(hours=10)
dec1 = Angle(degrees=30)
ra2 = Angle(hours=10.5)
dec2 = Angle(degrees=31)

pa = position_angle(ra1, dec1, ra2, dec2)
print(f"PA: {pa.degrees}°") # Measured N through E

Signature:

def position_angle(
ra1: Angle, dec1: Angle,
ra2: Angle, dec2: Angle,
verbose: Optional[VerboseContext] = None
) -> Angle

starward.core.time

JulianDate Class

Immutable Julian Date representation.

Creating JulianDates

from starward.core.time import JulianDate, jd_now

# Direct from JD value
jd = JulianDate(2460000.5)

# Current time
jd = jd_now()

# From components
jd = JulianDate.from_calendar(2024, 7, 4, 12, 0, 0) # July 4, 2024 12:00 UTC

# From datetime
from datetime import datetime, timezone
dt = datetime(2024, 7, 4, 12, 0, 0, tzinfo=timezone.utc)
jd = JulianDate.from_datetime(dt)

# From MJD
jd = JulianDate.from_mjd(60000.0)

# J2000.0 epoch
jd = JulianDate.j2000() # JD 2451545.0

Properties

jd = JulianDate(2460000.5)

jd.jd # 2460000.5 — Julian Date
jd.mjd # 60000.0 — Modified Julian Date
jd.t_j2000 # -0.0423... — Julian centuries since J2000.0
jd.days_j2000 # -1544.5 — Days since J2000.0

Methods

jd = JulianDate(2460000.5)

# To datetime
dt = jd.to_datetime() # datetime object (UTC)

# Sidereal time
gmst = jd.gmst() # Greenwich Mean Sidereal Time (hours)
lst = jd.lst(-118.25) # Local Sidereal Time for longitude (hours)

# With verbose output
from starward.verbose import VerboseContext
ctx = VerboseContext()
gmst = jd.gmst(verbose=ctx)
ctx.print_steps()

Arithmetic

jd1 = JulianDate(2460000.5)
jd2 = JulianDate(2460010.5)

# Add/subtract days
jd_later = jd1 + 10 # JulianDate(2460010.5)
jd_earlier = jd1 - 5 # JulianDate(2459995.5)

# Difference between JDs (returns days)
diff = jd2 - jd1 # 10.0

# Comparison
jd1 < jd2 # True
jd1 == jd2 # False

Convenience Functions

from starward.core.time import (
jd_now,
calendar_to_jd,
jd_to_datetime,
mjd_to_jd,
jd_to_mjd
)

# Current JD
jd = jd_now()

# Calendar to JD
jd_val = calendar_to_jd(2024, 7, 4, 12, 0, 0)

# JD to datetime
dt = jd_to_datetime(2460000.5)

# MJD conversions
jd_val = mjd_to_jd(60000.0) # 2460000.5
mjd_val = jd_to_mjd(2460000.5) # 60000.0

starward.core.coords

ICRSCoord Class

ICRS (J2000 equatorial) coordinates.

Creating

from starward.core.coords import ICRSCoord
from starward.core.angles import Angle

# From Angle objects
coord = ICRSCoord(
ra=Angle(hours=12.5),
dec=Angle(degrees=45.0)
)

# From degrees
coord = ICRSCoord.from_degrees(ra=187.5, dec=45.0)

# From string (most flexible)
coord = ICRSCoord.from_string("12h30m +45d")
coord = ICRSCoord.from_string("12h30m45s +45d30m15s")
coord = ICRSCoord.from_string("187.5 45.0")

Properties

coord = ICRSCoord.from_string("12h30m +45d")

coord.ra # Angle — Right Ascension
coord.dec # Angle — Declination
coord.ra.degrees # 187.5
coord.dec.degrees # 45.0

Transformations

from starward.core.coords import ICRSCoord
from starward.core.angles import Angle
from starward.core.time import jd_now

coord = ICRSCoord.from_string("12h30m +45d")

# To Galactic
gal = coord.to_galactic()
print(f"l={gal.l.degrees}°, b={gal.b.degrees}°")

# To Horizontal (requires observer location and time)
horiz = coord.to_horizontal(
latitude=Angle(degrees=34.05),
longitude=Angle(degrees=-118.25),
jd=jd_now()
)
print(f"Alt={horiz.alt.degrees}°, Az={horiz.az.degrees}°")

# to_icrs returns self
same = coord.to_icrs()

GalacticCoord Class

Galactic coordinates.

Creating

from starward.core.coords import GalacticCoord
from starward.core.angles import Angle

# From Angle objects
coord = GalacticCoord(
l=Angle(degrees=135.0),
b=Angle(degrees=71.6)
)

# From degrees
coord = GalacticCoord.from_degrees(l=0, b=0) # Galactic Center

# From ICRS
from starward.core.coords import ICRSCoord
icrs = ICRSCoord.from_string("12h30m +45d")
gal = GalacticCoord.from_icrs(icrs)

Transformations

gal = GalacticCoord.from_degrees(l=0, b=0)

# To ICRS
icrs = gal.to_icrs()
print(f"RA={icrs.ra.to_hms()}, Dec={icrs.dec.to_dms()}")

# to_galactic returns self
same = gal.to_galactic()

HorizontalCoord Class

Horizontal (Alt/Az) coordinates.

Creating

from starward.core.coords import HorizontalCoord
from starward.core.angles import Angle

# From Angle objects
coord = HorizontalCoord(
alt=Angle(degrees=45.0),
az=Angle(degrees=180.0)
)

# From degrees
coord = HorizontalCoord.from_degrees(alt=45.0, az=180.0)

# From ICRS (requires location and time)
from starward.core.coords import ICRSCoord
from starward.core.time import jd_now

icrs = ICRSCoord.from_string("12h30m +45d")
horiz = HorizontalCoord.from_icrs(
icrs,
latitude=Angle(degrees=34.05),
longitude=Angle(degrees=-118.25),
jd=jd_now()
)

Properties

horiz = HorizontalCoord.from_degrees(alt=45.0, az=180.0)

horiz.alt # Angle — Altitude
horiz.az # Angle — Azimuth
horiz.zenith # Angle — Zenith distance (90° - alt)
horiz.airmass # float — Atmospheric airmass (Pickering 2002)

transform_coords()

Generic coordinate transformation function.

from starward.core.coords import ICRSCoord, transform_coords
from starward.core.angles import Angle
from starward.core.time import jd_now

coord = ICRSCoord.from_string("12h30m +45d")

# Transform to Galactic
gal = transform_coords(coord, 'galactic')

# Transform to Horizontal
horiz = transform_coords(
coord,
'horizontal',
latitude=Angle(degrees=34.05),
longitude=Angle(degrees=-118.25),
jd=jd_now()
)

Signature:

def transform_coords(
coord: BaseCoord,
to_system: str, # 'icrs', 'galactic', 'horizontal', 'altaz'
latitude: Optional[Angle] = None,
longitude: Optional[Angle] = None,
jd: Optional[JulianDate] = None,
verbose: Optional[VerboseContext] = None
) -> BaseCoord

starward.core.constants

AstronomicalConstants Class

Singleton providing access to astronomical constants.

from starward.core.constants import AstronomicalConstants

const = AstronomicalConstants()

# Access constants as attributes
const.c # Constant object for speed of light
const.c.value # 299792458
const.c.unit # "m/s"
const.c.reference # "SI 2019 (exact)"
const.c.uncertainty # None (exact)

# Available constants
const.c # Speed of light
const.G # Gravitational constant
const.AU # Astronomical Unit
const.JD_J2000 # Julian Date of J2000.0
const.MJD_OFFSET # Modified JD offset
const.julian_year # Julian year (days)
const.julian_century # Julian century (days)
const.arcsec_per_rad # Arcseconds per radian
const.earth_radius_eq # Earth equatorial radius
const.earth_flattening # Earth flattening
const.earth_rotation_rate # Earth rotation rate
const.obliquity_j2000 # Mean obliquity at J2000.0
const.ra_ngp # RA of North Galactic Pole
const.dec_ngp # Dec of North Galactic Pole
const.l_ncp # Galactic lon of North Celestial Pole
const.M_sun # Solar mass
const.R_sun # Solar radius
const.L_sun # Solar luminosity

Methods

const = AstronomicalConstants()

# List all constants
for c in const.all():
print(f"{c.name}: {c.value} {c.unit or ''}")

# Search by name
for c in const.search("solar"):
print(c.name, c.value)

starward.verbose

VerboseContext Class

Collect calculation steps for educational output.

from starward.verbose import VerboseContext, step

# Create context
ctx = VerboseContext()

# Pass to calculations
from starward.core.angles import angular_separation, Angle
ra1, dec1 = Angle(hours=10), Angle(degrees=30)
ra2, dec2 = Angle(hours=11), Angle(degrees=31)
sep = angular_separation(ra1, dec1, ra2, dec2, verbose=ctx)

# Display steps
ctx.print_steps()

# Or get as string
output = ctx.format_steps()

# Or get as dict (for JSON)
data = ctx.to_dict()

# Clear and reuse
ctx.clear()

Adding Steps in Custom Code

from starward.verbose import VerboseContext, step

def my_calculation(x, y, verbose=None):
"""Custom calculation with verbose support."""

step(verbose, "Input", f"x = {x}, y = {y}")

result = x * y
step(verbose, "Multiply", f"x × y = {result}")

return result

# Use it
ctx = VerboseContext()
result = my_calculation(3, 4, verbose=ctx)
ctx.print_steps()

Complete Example

"""
Calculate when M31 is highest in the sky from a given location.
"""

from starward.core.coords import ICRSCoord, HorizontalCoord
from starward.core.time import JulianDate, jd_now
from starward.core.angles import Angle
from starward.verbose import VerboseContext

# M31 coordinates
m31 = ICRSCoord.from_string("00h42m44s +41d16m09s")

# Observer: New York City
lat = Angle(degrees=40.7128)
lon = Angle(degrees=-74.0060)

# Current position
jd = jd_now()
ctx = VerboseContext()

horiz = m31.to_horizontal(lat, lon, jd, verbose=ctx)

print(f"M31 from NYC at JD {jd.jd:.2f}:")
print(f" Altitude: {horiz.alt.to_dms()}")
print(f" Azimuth: {horiz.az.degrees:.1f}°")
print(f" Airmass: {horiz.airmass:.2f}")

if horiz.alt.degrees > 0:
print(" Status: VISIBLE")
else:
print(" Status: Below horizon")

# Show the math
print("\n--- Calculation Steps ---")
ctx.print_steps()

Type Hints

All starward functions have full type hints:

from starward.core.angles import Angle, angular_separation
from starward.core.time import JulianDate
from starward.verbose import VerboseContext
from typing import Optional

def angular_separation(
ra1: Angle,
dec1: Angle,
ra2: Angle,
dec2: Angle,
verbose: Optional[VerboseContext] = None
) -> Angle: ...

This enables IDE autocompletion and static type checking with mypy.


Error Handling

starward uses standard Python exceptions:

from starward.core.angles import Angle

try:
# Invalid: multiple units specified
a = Angle(degrees=45, hours=3)
except ValueError as e:
print(f"Error: {e}")

try:
# Invalid: unparseable string
a = Angle.parse("not an angle")
except ValueError as e:
print(f"Parse error: {e}")

For more information, see the source code or open an issue!