High-performance SGP4/SDP4 satellite propagation, powered by Zig with SIMD acceleration (AVX512/AVX2). Automatically handles both near-earth and deep-space satellites.
Platforms: macOS, Linux | Requires: Python 3.10+
Drop-in replacement for python-sgp4:
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)
# Batch propagation (270-330M props/sec with SIMD)
sat_array = SatrecArray(satrecs)
e, r, v = sat_array.sgp4(jd, fr) # Scalars or arrays
# Skip velocities for 30% faster propagation
e, r, _ = sat_array.sgp4(jd_array, fr_array, velocities=False)from astroz import propagate
import numpy as np
# Load and propagate - automatically optimized for 300M+ props/sec
positions = propagate("starlink", np.arange(1440)) # 1 day at 1-min intervals
# positions: (1440, num_satellites, 3) in km, ECEF coordinatesfrom 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
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)from astroz import screen
import numpy as np
times = np.arange(1440)
# Single target (fastest - uses fused propagate+screen, no full position array)
min_dists, min_t_indices = screen("starlink", times, threshold=50.0, target=0)
# Returns per-satellite minimum distance to target and time index
# All-vs-all screening
pairs, t_indices = screen("starlink", times, threshold=10.0)
# Returns all conjunction events within threshold# Before
from sgp4.api import Satrec, SatrecArray, jday
# After
from astroz.api import Satrec, SatrecArray, jdayThat's it. Your existing code works unchanged and runs 2x faster.
If you have loops, switch to batch methods:
# Before: loop (1.3M props/sec)
results = []
for jd, fr in zip(jd_list, fr_list):
e, r, v = sat.sgp4(jd, fr)
results.append(r)
# After: batch (15M props/sec) - 12x faster
jd_array = np.array(jd_list)
fr_array = np.array(fr_list)
e, r, v = sat.sgp4_array(jd_array, fr_array)# Before: loop over satellites (1.3M props/sec)
for sat in satellites:
e, r, v = sat.sgp4_array(jd, fr)
# After: batch all satellites (290M props/sec) - 100x faster
sat_array = SatrecArray(satellites)
e, r, v = sat_array.sgp4(jd, fr)# If you only need positions
e, r, _ = sat_array.sgp4(jd, fr, velocities=False)| Pattern | python-sgp4 | astroz | Speedup |
|---|---|---|---|
sat.sgp4() loop |
1.3M/s | 2.5M/s | 2x |
sat.sgp4_array() |
2.7M/s | 15M/s | 5x |
SatrecArray.sgp4() |
3M/s | 290M/s | 100x |
astroz supports the core python-sgp4 API for satellite propagation. Some rarely-used attributes are not implemented:
- TLE metadata:
classification,intldesg,elnum,revnum,ephtype - Intermediate elements:
Om,am,em,im,mm,nm,om(osculating elements after propagation) - Element rates:
argpdot,mdot,nodedot - Gravity constants:
j2,j3,j4,mu, etc. (available viaastroz.constants)
For typical satellite tracking and visualization, astroz is a full drop-in replacement.
| Constellation (13,478 sats x 1,440 steps) | Throughput |
|---|---|
| 1 thread | 37.7M props/sec |
| 16 threads | 303M props/sec |
Benchmarked on AMD Ryzen 7 7840U with AVX512.
Set ASTROZ_THREADS to control thread count (defaults to all cores).
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 (more efficient 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 velocityPropagate 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"
)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²")Requires Zig and Python 3.10+.
cd bindings/python
pip install -e .