Two-Line Elements (TLE)¶
Two-Line Element sets (TLEs) are a standardized format for representing satellite orbital data. Originally developed by NORAD (North American Aerospace Defense Command), TLEs encode an epoch, Keplerian orbital elements, and additional parameters needed for SGP4/SDP4 propagation into two 69-character ASCII lines.
An example of a TLE set for the International Space Station (ISS) is:
1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9995
2 25544 51.6347 1.5519 0004808 353.3325 6.7599 15.49579513535999
TLEs are still widely used for satellite tracking and orbit prediction, distributed by organizations like CelesTrak and Space-Track.
For additional information on the TLE format and field definitions, see the CelesTrak TLE documentation or the Wikipedia TLE article.
For complete API documentation, see the TLE reference.
TLE Accuracy Limitations
TLEs are designed for near-Earth satellites and have limited accuracy due to simplifications in the SGP4/SDP4 models. They ARE NOT suitable for high-precision orbit determination or long-term predictions.
NORAID ID Exhaustion
TLEs were originally designed for a maximum of 99,999 cataloged objects. However with the rise of mega-constellations and recent anti-satellite tests by Russia and China, the number of tracked objects is rapidly approaching this limit.
The Alpha-5 NORAD ID format extends the range by using letters A-Z (excluding I and O) as the leading character, allowing for up to 339,999 objects. This is a temporary solution however, and generally organizations should plan to transition to using formats like General Perturbations (GP) elements, CCSDS Orbit Ephemeris Messages (OEM), or other modern representations.
A common variant of TLEs is the Three-Line Element set (3LE), which adds a title line above the standard two lines for the object name. Brahe's TLE functions work with both TLE and 3LE formats interchangeably.
The same TLE data in 3LE format would be:
ISS (ZARYA)
1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9995
2 25544 51.6347 1.5519 0004808 353.3325 6.7599 15.49579513535999
Validating TLEs¶
Before parsing TLE data, you can validate the format and checksums to ensure data integrity.
Validating a TLE Set¶
import brahe as bh
# ISS TLE (NORAD ID 25544)
line1 = "1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9995"
line2 = "2 25544 51.6347 1.5519 0004808 353.3325 6.7599 15.49579513535999"
# Validate the complete TLE set (both lines must have matching NORAD IDs)
is_valid = bh.validate_tle_lines(line1, line2)
print(f"TLE set valid: {is_valid}")
# Validate individual lines
line1_valid = bh.validate_tle_line(line1)
line2_valid = bh.validate_tle_line(line2)
print(f"Line 1 valid: {line1_valid}")
print(f"Line 2 valid: {line2_valid}")
# Expected output:
# TLE set valid: True
# Line 1 valid: True
# Line 2 valid: True
use brahe as bh;
fn main() {
bh::initialize_eop().unwrap();
// ISS TLE (NORAD ID 25544)
let line1 = "1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9995";
let line2 = "2 25544 51.6347 1.5519 0004808 353.3325 6.7599 15.49579513535999";
// Validate the complete TLE set (both lines must have matching NORAD IDs)
let is_valid = bh::validate_tle_lines(line1, line2);
println!("TLE set valid: {}", is_valid);
// Validate individual lines
let line1_valid = bh::validate_tle_line(line1);
let line2_valid = bh::validate_tle_line(line2);
println!("Line 1 valid: {}", line1_valid);
println!("Line 2 valid: {}", line2_valid);
// Expected output:
// TLE set valid: true
// Line 1 valid: true
// Line 2 valid: true
}
The validate_tle_lines function checks that both lines have the correct format, valid checksums, and matching NORAD catalog numbers.
Calculating Checksums¶
Each TLE line ends with a modulo-10 checksum. You can calculate this checksum to verify data integrity or when creating custom TLEs:
import brahe as bh
# ISS TLE (NORAD ID 25544)
line1 = "1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9995"
line2 = "2 25544 51.6347 1.5519 0004808 353.3325 6.7599 15.49579513535999"
# Calculate checksums for each line
checksum1 = bh.calculate_tle_line_checksum(line1)
checksum2 = bh.calculate_tle_line_checksum(line2)
print(f"Line 1 checksum: {checksum1}")
print(f"Line 2 checksum: {checksum2}")
# Example with corrupted TLE (wrong checksum)
corrupted_line1 = (
"1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9990"
)
is_corrupted_valid = bh.validate_tle_line(corrupted_line1)
print(f"\nCorrupted line valid: {is_corrupted_valid}")
# Expected output:
# Line 1 checksum: 5
# Line 2 checksum: 9
#
# Corrupted line valid: False
use brahe as bh;
fn main() {
bh::initialize_eop().unwrap();
// ISS TLE (NORAD ID 25544)
let line1 = "1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9995";
let line2 = "2 25544 51.6347 1.5519 0004808 353.3325 6.7599 15.49579513535999";
// Calculate checksums for each line
let checksum1 = bh::calculate_tle_line_checksum(line1);
let checksum2 = bh::calculate_tle_line_checksum(line2);
println!("Line 1 checksum: {}", checksum1);
println!("Line 2 checksum: {}", checksum2);
// Example with corrupted TLE (wrong checksum)
let corrupted_line1 = "1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9990";
let is_corrupted_valid = bh::validate_tle_line(corrupted_line1);
println!("\nCorrupted line valid: {}", is_corrupted_valid);
// Expected output:
// Line 1 checksum: 5
// Line 2 checksum: 9
//
// Corrupted line valid: false
}
Checksum Algorithm
The checksum is calculated by summing all digits in the line (treating minus signs as 1) and taking the result modulo 10. All other characters (letters, spaces, periods) are ignored in the checksum calculation.
Parsing TLEs¶
Extracting Orbital Elements¶
The most common operation is parsing a TLE to extract the epoch and orbital elements:
import brahe as bh
# ISS TLE (NORAD ID 25544)
line1 = "1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9995"
line2 = "2 25544 51.6347 1.5519 0004808 353.3325 6.7599 15.49579513535999"
# Parse TLE to extract epoch and orbital elements
epoch, elements = bh.keplerian_elements_from_tle(line1, line2)
# Extract individual orbital elements
# Note: Angles are returned in degrees (exception to library convention)
a = elements[0] # Semi-major axis (m)
e = elements[1] # Eccentricity
i = elements[2] # Inclination (deg)
raan = elements[3] # Right Ascension of Ascending Node (deg)
argp = elements[4] # Argument of Periapsis (deg)
M = elements[5] # Mean Anomaly (deg)
print(f"ISS Orbital Elements (Epoch: {epoch})")
print(f" Semi-major axis: {a / 1000:.3f} km")
print(f" Eccentricity: {e:.6f}")
print(f" Inclination: {i:.4f} deg")
print(f" RAAN: {raan:.4f} deg")
print(f" Arg of Perigee: {argp:.4f} deg")
print(f" Mean Anomaly: {M:.4f} deg")
# Expected output:
# ISS Orbital Elements (Epoch: 2025-10-29 11:44:55.862 UTC)
# Semi-major axis: 6796.092 km
# Eccentricity: 0.000481
# Inclination: 51.6347 deg
# RAAN: 1.5519 deg
# Arg of Perigee: 353.3325 deg
# Mean Anomaly: 6.7599 deg
use brahe as bh;
fn main() {
bh::initialize_eop().unwrap();
// ISS TLE (NORAD ID 25544)
let line1 = "1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9995";
let line2 = "2 25544 51.6347 1.5519 0004808 353.3325 6.7599 15.49579513535999";
// Parse TLE to extract epoch and orbital elements
let (epoch, elements) = bh::keplerian_elements_from_tle(line1, line2).unwrap();
// Extract individual orbital elements
let a = elements[0]; // Semi-major axis (m)
let e = elements[1]; // Eccentricity
let i = elements[2]; // Inclination (deg)
let raan = elements[3]; // Right Ascension of Ascending Node (deg)
let argp = elements[4]; // Argument of Periapsis (deg)
let mean_anom = elements[5]; // Mean Anomaly (deg)
println!("ISS Orbital Elements (Epoch: {})", epoch);
println!(" Semi-major axis: {:.3} km", a / 1000.0);
println!(" Eccentricity: {:.6}", e);
println!(" Inclination: {:.4} deg", i);
println!(" RAAN: {:.4} deg", raan);
println!(" Arg of Perigee: {:.4} deg", argp);
println!(" Mean Anomaly: {:.4} deg", mean_anom);
// Expected output:
// ISS Orbital Elements (Epoch: 2025-10-29 11:44:55.862 UTC)
// Semi-major axis: 6796.092 km
// Eccentricity: 0.000481
// Inclination: 51.6347 deg
// RAAN: 1.5519 deg
// Arg of Perigee: 353.3325 deg
// Mean Anomaly: 6.7599 deg
}
The returned elements follow the standard Brahe order: [a, e, i, Ω, ω, M] where:
- \(a\) - Semi-major axis (meters)
- \(e\) - Eccentricity (dimensionless)
- \(i\) - Inclination (degrees)
- \(\Omega\) - Right Ascension of Ascending Node (degrees)
- \(\omega\) - Argument of Periapsis (degrees)
- \(M\) - Mean Anomaly (degrees)
Angle Units Convention
TLE functions use degrees for all angles. This matches the TLE format standard and makes it easier to work with TLE data directly.
Extracting Just the Epoch¶
If you only need the epoch timestamp without the full orbital elements:
import brahe as bh
# ISS TLE (NORAD ID 25544)
line1 = "1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9995"
# Extract epoch from line 1 (epoch is encoded in line 1 only)
epoch = bh.epoch_from_tle(line1)
print(f"TLE Epoch: {epoch}")
print(f"Time System: {epoch.time_system}")
# Expected output:
# TLE Epoch: 2025-10-29 11:44:55.862 UTC
# Time System: UTC
use brahe as bh;
fn main() {
bh::initialize_eop().unwrap();
// ISS TLE (NORAD ID 25544)
let line1 = "1 25544U 98067A 25302.48953544 .00013618 00000-0 24977-3 0 9995";
// Extract epoch from line 1 (epoch is encoded in line 1 only)
let epoch = bh::epoch_from_tle(line1).unwrap();
println!("TLE Epoch: {}", epoch);
println!("Time System: {:?}", epoch.time_system);
// Expected output:
// TLE Epoch: 2025-10-29 11:44:55.862 UTC
// Time System: UTC
}
The epoch is always returned in the UTC time system.
Creating TLEs¶
From Keplerian Elements¶
You can generate TLE lines from an epoch and mean orbital elements:
import brahe as bh
import numpy as np
# Define orbital epoch
epoch = bh.Epoch.from_datetime(2025, 10, 29, 11, 44, 55.766182, 0, bh.TimeSystem.UTC)
# Define ISS orbital elements
# Order: [a, e, i, raan, argp, M]
# Note: Angles must be in DEGREES for TLE creation (exception to library convention)
elements = np.array(
[
6795445.0, # Semi-major axis (m)
0.0004808, # Eccentricity
51.6347, # Inclination (deg)
1.5519, # Right Ascension of Ascending Node (deg)
353.3325, # Argument of Periapsis (deg)
6.7599, # Mean Anomaly (deg)
]
)
# Create TLE lines with NORAD ID
norad_id = "25544"
line1, line2 = bh.keplerian_elements_to_tle(epoch, elements, norad_id)
print("Generated TLE:")
print(line1)
print(line2)
# Expected output:
# Generated TLE:
# 1 25544U 25302.48953433 .00000000 00000+0 00000+0 0 00002
# 2 25544 51.6347 1.5519 0004808 353.3325 6.7599 15.49800901000006
use brahe as bh;
use nalgebra as na;
fn main() {
bh::initialize_eop().unwrap();
// Define orbital epoch
let epoch = bh::Epoch::from_datetime(
2025, 10, 29, 11, 44, 55.766182, 0.0, bh::TimeSystem::UTC
);
// Define ISS orbital elements
// Order: [a, e, i, raan, argp, M]
// Note: Angles must be in DEGREES for TLE creation (exception to library convention)
let elements = na::SVector::<f64, 6>::new(
6795445.0, // Semi-major axis (m)
0.0004808, // Eccentricity
51.6347, // Inclination (deg)
1.5519, // Right Ascension of Ascending Node (deg)
353.3325, // Argument of Periapsis (deg)
6.7599 // Mean Anomaly (deg)
);
// Create TLE lines with NORAD ID
let norad_id = "25544";
let (line1, line2) = bh::keplerian_elements_to_tle(&epoch, &elements, norad_id).unwrap();
println!("Generated TLE:");
println!("{}", line1);
println!("{}", line2);
// Expected output:
// Generated TLE:
// 1 25544U 25302.48953433 .00000000 00000+0 00000+0 0 00002
// 2 25544 51.6347 1.5519 0004808 353.3325 6.7599 15.49800901000006
}
Default Values
The keplerian_elements_to_tle function uses zero for fields like drag terms and derivatives. For complete control over all TLE fields, use the create_tle_lines function with its full parameter set.
Mean Element Representation
The TLE format encodes the orbital state as mean orbital elements estimated from orbit propgation using the SGP4/SDP4 models.
While the package allows for direclty creating TLEs from arbitrary Keplerian elements, the resulting TLEs WILL NOT accurate propagation results with the SGP4/SDP4 models unless the input elements are already mean elements derived from those models.
If you need to create TLEs for real satellites it's best to estimate the mean elements from observed data using orbit determination techniques using the SGP4/SDP4 models.
You can verify generated TLEs by parsing them back:
# Define orbital epoch
epoch = bh.Epoch.from_datetime(2025, 10, 29, 11, 44, 55.766182, 0, bh.TimeSystem.UTC)
# Define ISS orbital elements (angles in degrees)
elements = np.array(
[
6795445.0, # Semi-major axis (m)
0.0004808, # Eccentricity
51.6347, # Inclination (deg)
1.5519, # RAAN (deg)
353.3325, # Argument of Periapsis (deg)
6.7599, # Mean Anomaly (deg)
]
)
# Create TLE
// Define orbital epoch
let epoch = bh::Epoch::from_datetime(
2025, 10, 29, 11, 44, 55.766182, 0.0, bh::TimeSystem::UTC
);
// Define ISS orbital elements (angles in degrees)
let elements = na::SVector::<f64, 6>::new(
6795445.0, // Semi-major axis (m)
0.0004808, // Eccentricity
51.6347, // Inclination (deg)
1.5519, // RAAN (deg)
353.3325, // Argument of Periapsis (deg)
6.7599 // Mean Anomaly (deg)
);
// Create TLE
let norad_id = "25544";
let (line1, line2) = bh::keplerian_elements_to_tle(&epoch, &elements, norad_id).unwrap();
// Verify by parsing the generated TLE back
let (parsed_epoch, parsed_elements) = bh::keplerian_elements_from_tle(&line1, &line2).unwrap();
println!("Verification:");
println!("Epoch matches: {}", (epoch.jd() - parsed_epoch.jd()).abs() < 1e-9);
let elements_match = elements.iter()
.zip(parsed_elements.iter())
.all(|(a, b)| (a - b).abs() / a.abs().max(1e-10) < 1e-5);
println!("Elements match: {}", elements_match);
// Expected output:
NORAD ID Formats¶
TLEs support two formats for NORAD catalog numbers:
- Numeric: 5-digit numbers (00001-99999)
- Alpha-5: 5-character alphanumeric (A0000-Z9999)
The Alpha-5 format extends the catalog beyond 99,999 satellites by using letters A-Z (excluding I and O to avoid confusion with 1 and 0).
Converting Between Formats¶
import brahe as bh
print("NORAD ID Format Conversions\n")
# Convert numeric to Alpha-5 (only works for IDs >= 100000)
print("Numeric to Alpha-5:")
alpha5_low = bh.norad_id_numeric_to_alpha5(25544)
print(f" 25544 -> {alpha5_low}")
alpha5_high = bh.norad_id_numeric_to_alpha5(100000)
print(f" 100000 -> {alpha5_high}")
alpha5_higher = bh.norad_id_numeric_to_alpha5(123456)
print(f" 123456 -> {alpha5_higher}")
# Convert Alpha-5 to numeric
print("\nAlpha-5 to Numeric:")
numeric_1 = bh.norad_id_alpha5_to_numeric("A0001")
print(f" 'A0001' -> {numeric_1}")
numeric_2 = bh.norad_id_alpha5_to_numeric("L0000")
print(f" 'L0000' -> {numeric_2}")
# Round-trip conversion
print("\nRound-trip Conversion:")
original = 200000
alpha5 = bh.norad_id_numeric_to_alpha5(original)
back_to_numeric = bh.norad_id_alpha5_to_numeric(alpha5)
print(f" {original} -> '{alpha5}' -> {back_to_numeric}")
print(f" Match: {original == back_to_numeric}")
# Expected output:
# NORAD ID Format Conversions
#
bh::initialize_eop().unwrap();
println!("NORAD ID Format Conversions\n");
// Convert numeric to Alpha-5 (only works for IDs >= 100000)
println!("Numeric to Alpha-5:");
let alpha5_low = bh::norad_id_numeric_to_alpha5(25544).unwrap();
println!(" 25544 -> {}", alpha5_low);
let alpha5_high = bh::norad_id_numeric_to_alpha5(100000).unwrap();
println!(" 100000 -> {}", alpha5_high);
let alpha5_higher = bh::norad_id_numeric_to_alpha5(123456).unwrap();
println!(" 123456 -> {}", alpha5_higher);
// Convert Alpha-5 to numeric
println!("\nAlpha-5 to Numeric:");
let numeric_1 = bh::norad_id_alpha5_to_numeric("A0001").unwrap();
println!(" 'A0001' -> {}", numeric_1);
let numeric_2 = bh::norad_id_alpha5_to_numeric("L0000").unwrap();
println!(" 'L0000' -> {}", numeric_2);
// Round-trip conversion
println!("\nRound-trip Conversion:");
let original = 200000;
let alpha5 = bh::norad_id_numeric_to_alpha5(original).unwrap();
let back_to_numeric = bh::norad_id_alpha5_to_numeric(&alpha5).unwrap();
println!(" {} -> '{}' -> {}", original, alpha5, back_to_numeric);
println!(" Match: {}", original == back_to_numeric);
// Expected output:
// NORAD ID Format Conversions
//
// Numeric to Alpha-5:
// 25544 -> 25544
// 100000 -> A0000
Alpha-5 Range
Alpha-5 format is only valid for NORAD IDs >= 100,000. The range is 100,000 (A0000) to 339,999 (Z9999).
Parsing Mixed Formats¶
The parse_norad_id function automatically detects whether a NORAD ID is in numeric or Alpha-5 format:
bh::initialize_eop().unwrap();
// Parse NORAD IDs in different formats
println!("Parsing NORAD IDs:");
// Numeric format (standard)
let norad_numeric = bh::parse_norad_id("25544").unwrap();
println!(" '25544' -> {}", norad_numeric);
// Alpha-5 format (for IDs >= 100000)
let norad_alpha5 = bh::parse_norad_id("A0001").unwrap();
See Also¶
- SGP Propagator - Use TLEs with SGP4/SDP4 propagation
- Keplerian Elements - Working with orbital elements
- Downloading TLE Data - How to fetch current TLEs from online sources
- Epoch - Understanding time representation in Brahe