Skip to main content

urania

An astrodynamics library that computes, shows and explains

urania is a Python library for people who are not professionals but want to compute orbits seriously: CubeSat teams, students, space simulation game players. Every result records the model and assumptions it was computed with, and .explain() walks through the formulas and intermediate values step by step.

Status: MVP in development (v0.0.2, changelog). Design: SPEC.md. Design decisions: docs/DECISIONS.md.

Installation

Not on PyPI yet, so install from source. Python 3.10 or newer.

git clone https://github.com/dandmadp/urania.git
cd urania
python -m venv .venv
.venv\Scripts\activate          # macOS/Linux: source .venv/bin/activate
pip install -e ".[dev]"
pytest

Tested on Python 3.10 to 3.13, with both the newest and the oldest allowed dependency versions.

Quick start

The Δv needed to go from the ISS orbit to geostationary orbit (GEO):

import urania as ur

transfer = ur.ISS.transfer_to(ur.GEO)
print(transfer)                    # Transfer(hohmann, Δv=4.7797 km/s, tof=5.295 h, 2 burns)
print(transfer.explain())          # formulas, intermediate speeds, Δv per burn, assumptions
transfer.plot()                    # 2D plot (matplotlib)

Part of the explain() output:

3. Burn 1 (r₁)
   v₁ = √(μ/r₁) = 7.6573 km/s  (initial circular orbit)
   v₂ = √(μ(2/r₁ − 1/a_t)) = 10.0492 km/s  (transfer ellipse)
   combined with plane change α = 2.8913°
   Δv = √(v₁² + v₂² − 2 v₁ v₂ cos α) = 2.4325 km/s

Creating orbits

Pass values with units (astropy), or plain numbers, which are taken as SI (m, m/s, rad).

import astropy.units as u
import urania as ur

leo = ur.Orbit.circular(ur.Earth, 500 * u.km, inc=97.4 * u.deg)
ell = ur.Orbit.from_elements(ur.Earth, a=8000 * u.km, ecc=0.1, inc=30 * u.deg,
                             raan=40 * u.deg, argp=60 * u.deg, nu=0 * u.deg)
sv = ur.Orbit.from_vectors(ur.Earth, [6524.834, 6862.875, 6448.296] * u.km,
                           [4.901327, 5.533756, -1.976341] * u.km / u.s)

print(sv.a, sv.ecc, sv.period)     # results are SI numbers: m, -, s
print(sv.explain())                # how the elements follow from the state vector

# a 400 x 600 km orbit, at a UTC epoch
eo = ur.Orbit.from_apsides(ur.Earth, 400 * u.km, 600 * u.km, inc=51.6 * u.deg,
                           epoch=ur.Epoch.from_utc("2026-10-08T00:00:00Z"))

Presets: bodies Sun, Earth, Moon, Mars; orbits LEO (500 km), ISS (420 km, 51.64°), SSO (700 km sun-synchronous), GEO.

Propagation

Choose the fidelity: "twobody" (0) → "j2" (1) → "j2+drag" (2). The original object is unchanged; a new Trajectory is returned.

import astropy.units as u
import urania as ur

cubesat = dict(cd=2.2, area=0.03 * u.m**2, mass=4.0 * u.kg)
start = ur.Orbit.circular(ur.Earth, 400 * u.km, inc=51.6 * u.deg)

tr = start.propagate(days=3, model="j2+drag", **cubesat)
print(tr.final)                    # orbit after 3 days
print(tr.info.assumptions)         # model and assumptions used
print(tr.elements["raan"][-1])     # osculating elements at every sample, as arrays
tr.plot(kind="altitude")           # altitude with an orbit-averaged line

later = start.after(days=1, model="j2")   # just the orbit: shorthand for propagate(...).final

Passing area, mass or density without a drag term in model gives a warning, since drag would otherwise be left out silently.

You can inject environment data yourself: a constant, a function f(r, t), or an Environment object.

import urania as ur

start = ur.Orbit.circular(ur.Earth, 400e3)
kw = dict(days=1, model="drag", area=0.03, mass=4.0)

start.propagate(density=3e-12, **kw)                                       # constant [kg/m³]
start.propagate(density=lambda r, t: 2 * ur.Earth.atmosphere(r, t), **kw)  # function

Maneuvers

import astropy.units as u
import urania as ur

ur.LEO.transfer_to(ur.GEO)                                   # Hohmann transfer
far = ur.Orbit.circular(ur.Earth, 150000 * u.km)
ur.LEO.transfer_to(far, "bielliptic", rb=300000 * u.km)     # bi-elliptic transfer
ur.ISS.transfer_to(ur.GEO, plane_split=0)                    # whole plane change at apoapsis

When the planes differ, burns happen at the line of nodes, and by default a Hohmann transfer splits the plane change optimally between its two burns.

TLE

import urania as ur

iss = ur.TLEOrbit.from_text("""ISS (ZARYA)
1 25544U 98067A   19343.69339541  .00001764  00000-0  38792-4 0  9991
2 25544  51.6439 211.2001 0007417  17.6667  85.6398 15.50103472202482""")

sgp4 = iss.propagate(days=1)                          # SGP4 only
mine = iss.to_orbit().propagate(days=1, model="j2")   # extract the state, propagate with urania

TLE elements are SGP4-specific mean elements, so they are kept separate from Orbit. The frame is SGP4's TEME, used as is.

Accuracy and validation

What Reference Result
Element conversion Curtis examples 4.3, 4.7 / Vallado example 2-5 matches to the textbook's printed precision
Kepler's equation and propagation Vallado examples 2-1, 2-3, 2-4 within 1e-9 rad / 1 m
Hohmann and bi-elliptic transfers Vallado examples 6-1, 6-2 Δv within 1 mm/s
J2 secular drift analytic nodal rate within 1% over 10 days
Drag decay analytic circular decay da/dt = −ρB√(μa) within 1%
SGP4 wrapper Vallado SGP4 verification set (satellite 00005) within 1e-6 km
urania propagator vs SGP4 real ISS TLE (2019-12-09), J2 model at most 2.34 km over 24 hours
NASA GMAT R2026a, two-body 1 day, RK89 at most 0.009 m
NASA GMAT R2026a, J2 7 days, 420 km, 51.64° at most 3.3 m (144 m without the true pole, see below)
NASA GMAT R2026a, J2 + drag 3 days, 400 km, exponential atmosphere at most 3.3 m (73 m without the true pole)

SGP4 itself is accurate to about 1 km near epoch, so the SGP4 comparison is a sanity check rather than precise validation. Precise validation comes from the GMAT comparison (validation/gmat, tests/test_gmat.py).

GMAT evaluates J2 and the atmosphere about Earth's true spin axis, which at the J2000 epoch is 7.69" away from the J2000 z axis. urania uses the frame's z axis as the spin axis. The small figures above compare both in a frame whose z axis is the true pole; the larger figures in parentheses are the direct comparison in the J2000 frame.

Limitations (MVP)

  • Perturbations: J2 and atmospheric drag. Lunar/solar perturbations, solar radiation pressure and higher-order gravity come later.
  • Atmosphere: Vallado's exponential model (mean solar activity), with height above the reference ellipsoid. Real density varies several-fold with solar activity.
  • Maneuvers: impulsive transfers between circular orbits. Rendezvous (phasing) and interplanetary transfers come later.
  • Time: TDB internally. UTC goes through astropy: Epoch.from_utc(), epoch.utc, Epoch.now().
  • Frames: fixed inertial axes (precession and nutation ignored). TLEs use TEME as is.
  • Plots: 2D only.

Examples

License

MIT

Metadata

Release files for urania 0.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for urania 0.0.2
File Size Uploaded Provenance
urania-0.0.2.tar.gz 130.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for urania 0.0.2
File Interpreter ABI Platform
urania-0.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 177.6 kB

Release files / urania-0.0.2.tar.gz

Download URL urania-0.0.2.tar.gz
Size 130.1 kB
Tags Source
SHA-256 checksum
How to use checksums
0852240799b00f80559e75b2a0baaf21fef9917d100ee98019ea7d363b723f8c
BLAKE2b-256 checksum
How to use checksums
3d048f1653cf8cc7c9a97e2973bf7aa821ec178cada1bbe28ae2b8b12ed0fe80
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release files / urania-0.0.2-py3-none-any.whl

Download URL urania-0.0.2-py3-none-any.whl
Size 47.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c6202906ff78fe09f6c29905de9f5d9d6d61d44c625ccc72c5996b6700035402
BLAKE2b-256 checksum
How to use checksums
c3ee6b0180853e6f7d56f593b4250d4e6807b5a4316159d678598580b3e25381
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Security for urania 0.0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page