Skip to content

Latest commit

 

History

History
255 lines (189 loc) · 8.41 KB

README.md

File metadata and controls

255 lines (189 loc) · 8.41 KB

astroz Python Bindings

SGP4/SDP4 satellite propagation backed by the astroz Zig library, using SIMD (AVX512/AVX2) and threads. Near-earth and deep-space satellites are both handled.

Wheels: Linux (x86_64, arm64) and macOS (arm64), Python 3.10 to 3.14.

Quick Start

python-sgp4 compatible API

astroz.api follows the python-sgp4 API:

from astroz.api import Satrec, SatrecArray, jday, WGS72
import numpy as np

# Single satellite (same syntax as python-sgp4)
sat = Satrec.twoline2rv(line1, line2, WGS72)
jd, fr = jday(2024, 5, 6, 12, 0, 0.0)
error, position, velocity = sat.sgp4(jd, fr)

# Many satellites at once
sat_array = SatrecArray(satrecs)
e, r, v = sat_array.sgp4(jd, fr)  # Scalars or arrays

# Skip velocities if you only need positions (about 30% faster)
e, r, _ = sat_array.sgp4(jd_array, fr_array, velocities=False)

High-level API

from astroz import propagate
import numpy as np

positions = propagate("starlink", np.arange(1440))  # 1 day at 1-min intervals
# positions: (1440, num_satellites, 3) in km, ECEF coordinates

Loading Sources

from astroz import propagate, Constellation

# CelesTrak groups
positions = propagate("starlink", times)
positions = propagate("iss", times)
positions = propagate("gps", times)
positions = propagate("all", times)  # ~13k active satellites

# By NORAD ID
positions = propagate(None, times, norad_id=25544)  # ISS
positions = propagate(None, times, norad_id=[25544, 48274])  # Multiple

# Local file or URL (TLE or OMM JSON auto-detected)
positions = propagate("satellites.tle", times)
positions = propagate("https://example.com/tles.txt", times)

# OMM JSON (CCSDS 502.0-B-3) — supports 6+ digit NORAD IDs
positions = propagate('{"NORAD_CAT_ID":25544,"EPOCH":"2026-04-15T12:00:00.000000","MEAN_MOTION":15.489,...}', times)

# For repeated propagation, pre-parse to avoid overhead
c = Constellation("starlink")
positions1 = propagate(c, times1)
positions2 = propagate(c, times2)

Groups: all, starlink, oneweb, planet, spire, gps, glonass, galileo, beidou, stations/iss, weather, geo

Propagation

from astroz import propagate, Constellation
from datetime import datetime, timezone
import numpy as np

times = np.arange(1440)  # 1 day at 1-min intervals

# Simple (defaults: now UTC, ECEF output)
positions = propagate("starlink", times)

# With options
positions = propagate(
    "starlink",
    np.arange(14 * 1440),  # 2 weeks
    start_time=datetime(2024, 6, 1, tzinfo=timezone.utc),
    output="geodetic",  # "ecef" (default), "teme", or "geodetic"
)

# With velocities
positions, velocities = propagate("starlink", times, velocities=True)

Conjunction Screening

from astroz import screen

events = screen("starlink", 1440, threshold=5.0)  # one day, 5 km
for i, j, t, d in zip(events.i, events.j, events.time, events.distance):
    print(f"{i} and {j} pass {d:.3f} km apart at t+{t:.1f} min")

# Only encounters involving satellite 0
mine = screen("starlink", 1440, threshold=5.0, target=0)

screen returns every closest approach within the threshold, with its exact time, miss distance (km) and relative speed (km/s). Satellites are sampled on a grid (step, 1 minute by default), but each pair is checked with a bound on how far it can move between samples, so encounters between grid points are not missed. A coarser step is faster and still finds every encounter.

Migrating from python-sgp4

Changing the import is enough for existing code to run:

# Before
from sgp4.api import Satrec, SatrecArray, jday, WGS72
from sgp4 import exporter, omm

# After
from astroz.api import Satrec, SatrecArray, jday, WGS72
from astroz import exporter, omm

That covers Satrec.twoline2rv, Satrec() with sgp4init(...), sgp4/sgp4_array, SatrecArray, the element attributes, exporter.export_tle/export_omm, and omm.parse_csv/parse_xml/initialize. Gravity constants (WGS72OLD, WGS72, WGS84) and error codes (1, 2, 3, 4, 6) use python-sgp4's values, and a decayed satellite (error 6) still gets its position, as in python-sgp4. The test suite checks all of this against python-sgp4 itself.

Most of the speedup comes from the batch methods, so if your code loops over times or satellites, switch to sgp4_array or SatrecArray:

# Loop over times
for jd, fr in zip(jd_list, fr_list):
    e, r, v = sat.sgp4(jd, fr)
# becomes
e, r, v = sat.sgp4_array(np.array(jd_list), np.array(fr_list))

# Loop over satellites
for sat in satellites:
    e, r, v = sat.sgp4_array(jd, fr)
# becomes
e, r, v = SatrecArray(satellites).sgp4(jd, fr)

Pass velocities=False to SatrecArray.sgp4 if you only need positions.

Method python-sgp4 astroz Speedup
sat.sgp4() in a loop 1.3M/s 2.5M/s 2x
sat.sgp4_array() 2.7M/s 15M/s 5x
SatrecArray.sgp4() 3M/s 290M/s 100x

Compatibility notes

  • sgp4init implements the improved operation mode, 'i' (python-sgp4's default); 'a' raises ValueError. It also accepts the catalog number as a TLE field string such as "A0404".
  • SatrecArray matches python-sgp4 to millimetres for near-earth orbits and to metres for deep-space ones, where the SIMD batch path's trigonometry adds a few parts per billion. Satrec.sgp4 matches to the micrometre.
  • These Satrec attributes are not implemented:
    • Elements after propagation: Om, am, em, im, mm, nm, om
    • Element rates: argpdot, mdot, nodedot
    • Gravity constants on the object: j2, j3, j4, mu, etc. Earth constants are available as astroz.EARTH_MU, astroz.EARTH_J2 and astroz.EARTH_R_EQ.

Performance

Constellation (13,478 sats x 1,440 steps) Throughput
1 thread 37.7M props/sec
16 threads 303M props/sec

Measured on an AMD Ryzen 7 7840U with AVX512.

Set ASTROZ_THREADS to control thread count (defaults to all cores).

Orbital Mechanics

from astroz import (
    hohmann_transfer, bi_elliptic_transfer, lambert,
    orbital_velocity, orbital_period, escape_velocity,
    EARTH_MU,
)

# Hohmann transfer: LEO (400 km) to GEO
result = hohmann_transfer(EARTH_MU, 6778, 42164)
print(f"Total ΔV: {result['total_dv']:.3f} km/s")
print(f"Transfer time: {result['transfer_time_days']:.2f} days")

# Bi-elliptic transfer (cheaper than Hohmann for large radius ratios)
result = bi_elliptic_transfer(EARTH_MU, 6778, 42164, 100000)
print(f"Total ΔV: {result['total_dv']:.3f} km/s")

# Lambert solver: find transfer orbit between two positions
r1 = (7000.0, 0.0, 0.0)
r2 = (0.0, 7000.0, 0.0)
tof = 3600.0  # 1 hour
result = lambert(EARTH_MU, r1, r2, tof)
print(f"Departure velocity: {result['departure_velocity']}")

# Orbital velocity, period, escape velocity
v = orbital_velocity(EARTH_MU, 6778)          # Circular orbit at 400 km
T = orbital_period(EARTH_MU, 6778)            # Period in seconds
v_esc = escape_velocity(EARTH_MU, 6778)       # Escape velocity

Numerical Propagation

Propagate orbits with perturbation models (J2, atmospheric drag) using RK4 or Dormand-Prince 8(7) adaptive integrators. r_eq is required when using J2 or drag perturbations. Drag uses an exponential Earth atmosphere model (sea-level density 1.225 kg/m^3, scale height 7.249 km, cutoff altitude 1500 km). drag_area is in m^2 and drag_mass in kg.

from astroz import propagate_numerical, EARTH_MU, EARTH_J2, EARTH_R_EQ

# Initial state: LEO circular orbit [x, y, z, vx, vy, vz] (km, km/s)
state = (6778.0, 0.0, 0.0, 0.0, 7.668, 0.0)

# Two-body propagation (1 orbit, 60s steps)
times, states = propagate_numerical(state, 0.0, 5554.0, 60.0, EARTH_MU)

# With J2 perturbation
times, states = propagate_numerical(
    state, 0.0, 5554.0, 60.0, EARTH_MU,
    j2=EARTH_J2, r_eq=EARTH_R_EQ,
)

# With J2 + atmospheric drag
times, states = propagate_numerical(
    state, 0.0, 86400.0, 60.0, EARTH_MU,
    j2=EARTH_J2, r_eq=EARTH_R_EQ,
    drag_cd=2.2, drag_area=10.0, drag_mass=500.0,
    integrator="dp87",  # or "rk4"
)

Constants

from astroz import EARTH_MU, EARTH_R_EQ, EARTH_J2, SUN_MU, MOON_MU

print(f"Earth μ: {EARTH_MU} km³/s²")
print(f"Earth radius: {EARTH_R_EQ} km")
print(f"Earth J2: {EARTH_J2}")
print(f"Sun μ: {SUN_MU} km³/s²")
print(f"Moon μ: {MOON_MU} km³/s²")

Building

Requires Zig and Python 3.10+.

cd bindings/python
pip install -e .