Skip to content

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:

import brahe as bh

# Parse NORAD IDs in different formats
print("Parsing NORAD IDs:")

# Numeric format (standard)
norad_numeric = bh.parse_norad_id("25544")
print(f"  '25544' -> {norad_numeric}")
    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