Brahe is a modern satellite dynamics library for research and engineering applications. It is designed to be quick-to-deploy, composable, extensible, and easy-to-learn. The north-star of the development is enabling users to solve meaningful problems quickly and correctly.
Brahe is permissively licensed under an MIT License to enable people to use and build on the work without worrying about licensing restrictions. We want people to be able to stop reinventing the astrodynamics "wheel" because commercial licenses are expensive and open-source options are hard to use.
We try to prioritize making the software library easy to learn, use, and verify. Many astrodynamics libraries are written with many layers of abstraction for flexibility that can make it challenging for new users to understand where the actual logic and algorithms are being executed. Brahe is written in a modern style with an emphasis on code clarity and modularity to make it easier to understand what individual functions are actually doing. This approach has the added benefit of making it easier to verify and validate the correctness of the implementation.
If you do find this useful, please consider starring the repository on GitHub to help increase its visibility. If you're using Brahe for school, research, a commercial endeavour, or flying a mission. I'd love to know about it.
If you want to learn more about how to use the package the documentation is structured in the following way:
Getting Started: The getting started guide provides a high-level overview of the main concepts and components of Brahe, along with a quick introduction to using it. It is designed to help new users get up to speed quickly and understand the core ideas behind Brahe before diving into the more detailed documentation in the other sections. If you are new to Brahe, this is a great place to start!
User Guide: The user guide provides more comprehensive module-by-module documentation covering the capabilities each module provides with examples on how to use it.
Examples: This section contains a collection of worked examples that demonstrate how to use Brahe to solve various problems or accomplish specific tasks. The examples are designed to be practical and cover a range of use cases, from basic to more advanced.
Python API Reference: Provides detailed reference documentation of the Python API, including all public classes, functions, and methods organized by module.
Rust API Reference: Provides detailed reference documentation of the Rust API, including all public structs, traits, functions, and methods organized by module.
importbraheasbh# Define the semi-major axis of a low Earth orbit (in meters)a=bh.constants.R_EARTH+400e3# 400 km altitude# Calculate the orbital periodT=bh.orbital_period(a)print(f"Orbital Period: {T/60:.2f} minutes")
usebrahe::{R_EARTH,orbital_period};fnmain(){// Define the semi-major axis of a low Earth orbit (in meters)letsemi_major_axis=R_EARTH+400e3;// 400 km altitude// Calculate the orbital periodletperiod=orbital_period(semi_major_axis);println!("Orbital Period: {:.2} minutes",period/60.0);}
importbraheasbhbh.initialize_eop()# Fetch the ISS from CelesTrak and create a propagatorclient=bh.celestrak.CelestrakClient()iss=client.get_sgp_propagator(catnr=25544,step_size=60.0)# Compute upcoming passes of the ISS over San Franciscopasses=bh.location_accesses(bh.PointLocation(-122.4194,37.7749,0.0),# San Franciscoiss,bh.Epoch.now(),bh.Epoch.now()+24*3600.0,# Next 24 hoursbh.ElevationConstraint(min_elevation_deg=10.0),)print(f"Number of passes in next 24 hours: {len(passes)}")
@article{eddy2026brahe,title={{Brahe: A Modern Astrodynamics Library for Research and Engineering Applications}},author={Duncan Eddy and Mykel J. Kochenderfer},year={2026},eprint={2601.06452},archivePrefix={arXiv},primaryClass={astro-ph.IM},url={https://arxiv.org/abs/2601.06452},}
Brahe follows a versioning scheme modeled on NumPy's policy rather than strict SemVer. Versions are PEP 440 compliant and take the form major.minor.bugfix:
Major releases (X.0.0) are rare and signal significant API or ABI breaks.
Minor releases (1.Y.0) contain new features, deprecations, and removals of previously deprecated code.
Bugfix releases (1.2.Z) contain only fixes — no new features, deprecations, or removals.
Deprecation policy (transitional): The long-term target — matching NumPy — is that backwards-incompatible API changes emit a DeprecationWarning for at least two minor releases before removal. While Brahe is in its early adoption phase, a deprecation may occur and be removed within a single minor release. This window will expand to multiple minor releases with deprecation warnings as adoption grows.
Pinning: For most projects brahe>=1.2 is sufficient. If you need guaranteed stability during the transitional deprecation period, pin to a specific major.minor.patch version (e.g., 1.2.3) rather than using a floating specifier (e.g., ^1.2.0 or >=1.2.0). See the versioning page for the full policy, including guidance on treating DeprecationWarning as an error in CI.
The project is licensed under the MIT License - see the LICENSE for details.
We want to make it easy for people to use and build on the work without worrying about licensing restrictions.
Additionally, brahe uses cargo-deny to confirm that all dependencies are permissively licensed and compatible with commercial use. The permitted licenses can be found in the cargo-deny configuration file.
If you find a bug, have a feature request, want to contribute, please open an issue or a pull request on the GitHub repository. Contributions are welcome and encouraged! If you see something missing, but don't know how to start contributing, please open an issue and we can discuss it. We are building software to help everyone on this planet explore the universe. We encourage you to bring your unique perspective to help make us stronger. We appreciate contributions from everyone, no prior space experience is needed to participate.
The development of Brahe has roots in 2014 when I first started writing astrodynamics software for my PhD. The main algorithms and code structure evolved over the years based on my own experience applying the software to both research problems and operational space missions. The core functionality of the library (time handling, reference frames, reference frame transformations, coordinate transformations) were all developed before the usage of AI tools. AI tools have since been intentionally adopted to help with improving and expanding capabilities that were on the nice-to-have feature list. They have also been used to help with writing documentation and improve code coverage. All results and outputs are manually reviewed, run, tested, and verified manually before being merged into the main branch, we expect the same from all contributions to the codebase. We are committed to maintaining the same standards of code clarity, modularity, and correctness for all contributions regardless of whether they were AI-assisted or not.
The use of AI-assisted coding in brahe is itself a bit of an expertiment. We are interesting in seeing how it can be used to help with the development of the library, however we will not compromise on the quality of the codebase overall. While we may get it wrong at times, times, producing correct, accurate, maintainable code is more important than producing code quickly.
For new contributions, we allow the use of AI-assited coding, however we expect that PRs will be manually reviewed and tested before being submitted and that all PRs follow the same standards of code clarity, modularity, and correctness as the rest of the codebase.
If you want to see more examples of how to use brahe, you can find a few quick examples below. You will also find more examples throughout the documentation and in the Examples section of the documentation.
importbraheasbh# Create an epoch from a specific date and timeepc=bh.Epoch(2024,1,1,12,0,0.0,time_system=bh.TimeSystem.UTC)# Print as ISO 8601 stringprint(f"Epoch in UTC: {epc.isostring()}")mjd_tai=epc.mjd_as_time_system(bh.TimeSystem.TAI)print(f"MJD in TAI: {mjd_tai}")jd_gps=epc.jd_as_time_system(bh.TimeSystem.GPS)print(f"JD in GPS: {jd_gps}")epc2=bh.Epoch(2024,1,2,13,30,0.0,time_system=bh.TimeSystem.GPS)delta_seconds=epc2-epcprint(f"Difference between epochs in seconds: {delta_seconds}")epc_utc=epc2.to_string_as_time_system(bh.TimeSystem.UTC)print(f"Epoch in GPS: {epc2}")print(f"Epoch in UTC: {epc_utc}")
usebrahe::{Epoch,TimeSystem};fnmain(){// Create an epoch from a specific date and timeletepc=Epoch::from_datetime(2024,1,1,12,0,0.0,0.0,TimeSystem::UTC);// Print as ISO 8601 stringprintln!("Epoch in UTC: {}",epc.isostring());letmjd_tai=epc.mjd_as_time_system(TimeSystem::TAI);println!("MJD in TAI: {}",mjd_tai);letjd_gps=epc.jd_as_time_system(TimeSystem::GPS);println!("JD in GPS: {}",jd_gps);letepc2=Epoch::from_datetime(2024,1,2,13,30,0.0,0.0,TimeSystem::GPS);letdelta_seconds=epc2-epc;println!("Difference between epochs in seconds: {}",delta_seconds);letepc_utc=epc2.to_string_as_time_system(TimeSystem::UTC);println!("Epoch in GPS: {}",epc2);println!("Epoch in UTC: {}",epc_utc);}
importbraheasbhimportnumpyasnp# Initialize Earth Orientation Parameter databh.initialize_eop()# Define orbital elementsa=bh.constants.R_EARTH+700e3# Semi-major axis in meters (700 km altitude)e=0.001# Eccentricityi=98.7# Inclination in radiansraan=15.0# Right Ascension of Ascending Node in radiansarg_periapsis=30.0# Argument of Periapsis in radiansmean_anomaly=45.0# Mean Anomaly# Create a state vector from orbital elementsstate_kep=np.array([a,e,i,raan,arg_periapsis,mean_anomaly])# Convert Keplerian state to ECI coordinatesstate_eci=bh.state_koe_to_eci(state_kep,bh.AngleFormat.DEGREES)print(f"ECI Coordinates: {state_eci}")epoch=bh.Epoch(2024,6,1,12,0,0.0,time_system=bh.TimeSystem.UTC)# Convert ECI coordinates to ECEF coordinates at the given epochstate_ecef=bh.state_eci_to_ecef(epoch,state_eci)print(f"ECEF Coordinates: {state_ecef}")state_eci_2=bh.state_ecef_to_eci(epoch,state_ecef)print(f"Recovered ECI Coordinates: {state_eci_2}")state_kep_2=bh.state_eci_to_koe(state_eci_2,bh.AngleFormat.DEGREES)print(f"Recovered Keplerian Elements: {state_kep_2}")
usebraheasbh;usebrahe::{Epoch,TimeSystem,R_EARTH,state_koe_to_eci,state_eci_to_ecef,state_ecef_to_eci,state_eci_to_koe,AngleFormat};usenalgebra::Vector6;fnmain(){// Initialize EOPbh::initialize_eop().unwrap();// Define orbital elementsleta=R_EARTH+700e3;// Semi-major axis in meters (700 km altitude)lete=0.001;// Eccentricityleti=98.7;// Inclination in degreesletraan=15.0;// Right Ascension of Ascending Node in degreesletarg_periapsis=30.0;// Argument of Periapsis in degreesletmean_anomaly=45.0;// Mean Anomaly in degrees// Create a state vector from orbital elementsletstate_kep=Vector6::new(a,e,i,raan,arg_periapsis,mean_anomaly);// Convert Keplerian state to ECI coordinatesletstate_eci=state_koe_to_eci(state_kep,AngleFormat::Degrees);println!("ECI Coordinates: {:?}",state_eci);letepoch=Epoch::from_datetime(2024,6,1,12,0,0.0,0.0,TimeSystem::UTC);// Convert ECI coordinates to ECEF coordinates at the given epochletstate_ecef=state_eci_to_ecef(epoch,state_eci);println!("ECEF Coordinates: {:?}",state_ecef);letstate_eci_2=state_ecef_to_eci(epoch,state_ecef);println!("Recovered ECI Coordinates: {:?}",state_eci_2);letstate_kep_2=state_eci_to_koe(state_eci_2,AngleFormat::Degrees);println!("Recovered Keplerian Elements: {:?}",state_kep_2);}
importnumpyasnpimportbraheasbh# Define the initial Keplerian elementsa=bh.constants.R_EARTH+700e3# Semi-major axis: 700 km altitudee=0.001# Eccentricityi=98.7# Inclination in degreesraan=15.0# Right Ascension of Ascending Node in degreesargp=30.0# Argument of Perigee in degreesmean_anomaly=75.0# Mean Anomaly at epoch in degreesinitial_state=np.array([a,e,i,raan,argp,mean_anomaly])# Define the epoch timeepoch=bh.Epoch.now()# Create the Keplerian Orbit Propagatordt=60.0# Time step in secondspropagator=bh.KeplerianPropagator.from_keplerian(epoch,initial_state,bh.AngleFormat.DEGREES,dt)# Propagate the orbit for 3 time stepspropagator.propagate_steps(3)# States are stored as a Trajectory objectassertlen(propagator.trajectory)==4# Initial state + 3 propagated states# Convert trajectory to ECI coordinateseci_trajectory=propagator.trajectory.to_eci()# Iterate over all stored statesforepoch,stateineci_trajectory:print(f"Epoch: {epoch}, Position (ECI): {state[0]/1e3:.2f} km, {state[1]/1e3:.2f} km, {state[2]/1e3:.2f} km")end_epoch=epoch+86400*7# 7 days laterpropagator.propagate_to(end_epoch)# Confirm the final epoch is as expectedassertabs(propagator.current_epoch()-end_epoch)<1e-6print("Propagation complete. Final epoch:",propagator.current_epoch())
usebraheasbh;usebrahe::{Epoch,R_EARTH,KeplerianPropagator,AngleFormat};usebrahe::traits::{SStatePropagator,Trajectory};usenalgebra::Vector6;fnmain(){// Define the initial Keplerian elementsleta=R_EARTH+700e3;// Semi-major axis: 700 km altitudelete=0.001;// Eccentricityleti=98.7;// Inclination in degreesletraan=15.0;// Right Ascension of Ascending Node in degreesletargp=30.0;// Argument of Perigee in degreesletmean_anomaly=75.0;// Mean Anomaly at epoch in degreesletinitial_state=Vector6::new(a,e,i,raan,argp,mean_anomaly);// Define the epoch timeletepoch=Epoch::now();// Create the Keplerian Orbit Propagatorletdt=60.0;// Time step in secondsletmutpropagator=KeplerianPropagator::from_keplerian(epoch,initial_state,AngleFormat::Degrees,dt);// Propagate the orbit for 3 time stepspropagator.propagate_steps(3);// States are stored as a Trajectory objectassert_eq!(propagator.trajectory.len(),4);// Initial state + 3 propagated states// Convert trajectory to ECI coordinatesleteci_trajectory=propagator.trajectory.to_eci();// Iterate over all stored statesforiin0..eci_trajectory.len(){letepoch=eci_trajectory.epochs[i];letstate=eci_trajectory.states[i].clone();println!("Epoch: {}, Position (ECI): {:.2} km, {:.2} km, {:.2} km",epoch,state[0]/1e3,state[1]/1e3,state[2]/1e3);}// Output (will vary based on current time):// Epoch: 2025-10-24 22:14:56.707 UTC, Position (ECI): -1514.38 km, -1475.59 km, 6753.03 km// Epoch: 2025-10-24 22:15:56.707 UTC, Position (ECI): -1935.70 km, -1568.01 km, 6623.80 km// Epoch: 2025-10-24 22:16:56.707 UTC, Position (ECI): -2349.19 km, -1654.08 km, 6467.76 km// Epoch: 2025-10-24 22:17:56.707 UTC, Position (ECI): -2753.17 km, -1733.46 km, 6285.55 km// Propagate for 7 daysletend_epoch=epoch+86400.0*7.0;// 7 days laterpropagator.propagate_to(end_epoch);// Confirm the final epoch is close to expected timelettime_diff=(propagator.current_epoch()-end_epoch).abs();assert!(time_diff<1.0e-6,"Final epoch should be within 1 second of target");println!("Propagation complete. Final epoch: {}",propagator.current_epoch());// Output (will vary based on current time):// Propagation complete. Final epoch: 2025-10-31 22:18:40.413 UTC}
importbraheasbh# Initialize EOPbh.initialize_eop()# Set the locationlocation=bh.PointLocation(-122.4194,37.7749,0.0).with_name("San Francisco")# Get the latest TLE for the ISS (NORAD ID 25544) from Celestrakclient=bh.celestrak.CelestrakClient()propagator=client.get_sgp_propagator(catnr=25544,step_size=60.0)# Configure Search Windowepoch_start=bh.Epoch.now()epoch_end=epoch_start+7*86400.0# 7 days later# Set access constraints -> Must be above 10 degrees elevationconstraint=bh.ElevationConstraint(min_elevation_deg=10.0)# Compute access windowswindows=bh.location_accesses(location,propagator,epoch_start,epoch_end,constraint)assertlen(windows)>0,"Should find at least one access window"# Print first 3 access windowsforwindowinwindows[:3]:print(f"Access Window: {window.window_open} to {window.window_close}, Duration: {window.duration/60:.2f} minutes")
usebraheasbh;usebrahe::{Epoch,PointLocation,ElevationConstraint,location_accesses};usebrahe::celestrak::CelestrakClient;usebrahe::utils::Identifiable;fnmain(){// Initialize EOPbh::initialize_eop().unwrap();// Set the locationletlocation=PointLocation::new(-122.4194,37.7749,0.0).with_name("San Francisco");// Get the latest TLE for the ISS (NORAD ID 25544) from Celestrakletclient=CelestrakClient::new();letpropagator=client.get_sgp_propagator_by_catnr(25544,60.0).unwrap();// Configure Search Windowletepoch_start=Epoch::now();letepoch_end=epoch_start+7.0*86400.0;// 7 days later// Set access constraints -> Must be above 10 degrees elevationletconstraint=ElevationConstraint::new(Some(10.0),None).unwrap();// Compute access windowsletwindows=location_accesses(&location,&propagator,epoch_start,epoch_end,&constraint,None,None,).unwrap();assert!(!windows.is_empty(),"Should find at least one access window");// Print first 3 access windowsforwindowinwindows.iter().take(3){println!("Access Window: {} to {}, Duration: {:.2} minutes",window.window_open,window.window_close,window.duration()/60.0);}}