Sidereon.Format.TLE (Sidereon v3.0.0)

Copy Markdown View Source

Parse and encode Two-Line Element sets.

TLE is the legacy fixed-width format for satellite orbital elements, designed for 80-column punch cards in the 1960s. Despite its age, it remains the most widely used format for distributing orbital data.

The format grammar lives in the Rust core (sidereon_core::astro::tle): fixed-width field extraction and validation, the modulo-10 checksum, the "assumed decimal" drag-term codec, per-field number formatting, and the two-digit-year pivot. This module keeps the Sidereon API shape: it marshals the epoch between its native DateTime and the (epoch_year, epoch_day_of_year) pair the core exposes, applies input defaults, logs advisory checksum warnings, and maps errors.

Parsing

The parser is liberal in what it accepts:

  • Trailing whitespace and extra characters are trimmed
  • Leading dots in floats (.123 → 0.123)
  • Spaces in numeric fields

Column 69 holds the modulo-10 checksum of columns 1-68. Under the default policy: :strict, a digit that disagrees with the checksum, or a column 69 that is not a digit, is refused. policy: :lenient reads such a line, as Vallado's twoline2rv does, and reports each finding as a checksum warning. A line that ends before column 69 carries no checksum; it is read and reported under both policies.

Examples

{:ok, elements} = Sidereon.Format.TLE.parse(line1, line2)
{:ok, {line1, line2}} = Sidereon.Format.TLE.encode(elements)

Summary

Types

A line whose column 69 did not confirm its checksum: the line label ("line 1" or "line 2"), what column 69 held ({:mismatch, digit}, {:not_digit, character} or :missing), and the checksum computed from columns 1-68.

A satellite read from a TLE file.

How column 69, the line checksum, is treated.

Why a stretch of a TLE file did not become a satellite: {:invalid, reason} for an element set refused by the TLE grammar, the checksum policy or SGP4 initialization, :missing_line_2 for a line 1 with no line 2 after it, :orphan_line_2 for a line 2 with no line 1 before it, and :orphan_name for a name line not followed by an element set.

A rejected stretch of a TLE file, with the one-based line number of its first line.

Functions

Encode an %Sidereon.Elements{} struct as TLE-format strings.

Like encode/1 but raises on malformed elements.

Parse a two-line element set into an %Sidereon.Elements{} struct.

Parse a multi-record TLE file (CelesTrak / Space-Track style).

Parse a two-line element set, returning the checksum warnings the policy accepted alongside the elements instead of logging them.

Types

checksum_warning()

@type checksum_warning() ::
  {String.t(), {:mismatch, 0..9} | {:not_digit, String.t()} | :missing, 0..9}

A line whose column 69 did not confirm its checksum: the line label ("line 1" or "line 2"), what column 69 held ({:mismatch, digit}, {:not_digit, character} or :missing), and the checksum computed from columns 1-68.

encode_error()

@type encode_error() ::
  {:missing_field, atom()}
  | {:invalid_field, atom(), term()}
  | {:encode_error, Sidereon.CCSDS.Error.tle()}
  | Sidereon.argument_error()

file_satellite()

@type file_satellite() :: %{
  name: String.t(),
  tle: Sidereon.Elements.t(),
  line_number: pos_integer(),
  checksum_warnings: [checksum_warning()]
}

A satellite read from a TLE file.

policy()

@type policy() :: :strict | :lenient

How column 69, the line checksum, is treated.

record_issue()

@type record_issue() ::
  {:invalid, Sidereon.CCSDS.Error.sgp4()}
  | :missing_line_2
  | :orphan_line_2
  | :orphan_name

Why a stretch of a TLE file did not become a satellite: {:invalid, reason} for an element set refused by the TLE grammar, the checksum policy or SGP4 initialization, :missing_line_2 for a line 1 with no line 2 after it, :orphan_line_2 for a line 2 with no line 1 before it, and :orphan_name for a name line not followed by an element set.

rejected_record()

@type rejected_record() :: %{
  line_number: pos_integer(),
  name: String.t(),
  issue: record_issue()
}

A rejected stretch of a TLE file, with the one-based line number of its first line.

Functions

encode(el)

@spec encode(Sidereon.Elements.t()) ::
  {:ok, {String.t(), String.t()}} | {:error, encode_error()}

Encode an %Sidereon.Elements{} struct as TLE-format strings.

Returns {:ok, {line1, line2}}: two 69-character strings with valid checksums, or {:error, reason} for malformed elements. Round-trips are character-exact for standard TLEs. A TLE states a catalog number, so elements without one, as Sidereon.Format.OMM.to_elements/1 gives for an OMM without NORAD_CAT_ID, are refused with {:error, {:missing_field, :catalog_number}}.

Examples

iex> l1 = "1 25544U 98067A   18184.80969102  .00001614  00000-0  31745-4 0  9993"
iex> l2 = "2 25544  51.6414 295.8524 0003435 262.6267 204.2868 15.54005638121106"
iex> {:ok, el} = Sidereon.Format.TLE.parse(l1, l2)
iex> {:ok, {gen_l1, gen_l2}} = Sidereon.Format.TLE.encode(el)
iex> gen_l1 == l1
true
iex> gen_l2 == l2
true

encode!(el)

@spec encode!(Sidereon.Elements.t()) :: {String.t(), String.t()}

Like encode/1 but raises on malformed elements.

parse(longstr1, longstr2, opts \\ [])

@spec parse(String.t(), String.t(), keyword()) ::
  {:ok, Sidereon.Elements.t()}
  | {:error, Sidereon.CCSDS.Error.tle() | {:invalid_field, :policy, term()}}

Parse a two-line element set into an %Sidereon.Elements{} struct.

Returns {:ok, elements} or {:error, reason}. Each checksum warning the policy accepts is logged; parse_with_warnings/3 returns them instead.

Options

  • :policy - :strict (default) or :lenient; see the module documentation.

Examples

iex> {:ok, el} = Sidereon.Format.TLE.parse(
...>   "1 25544U 98067A   18184.80969102  .00001614  00000-0  31745-4 0  9993",
...>   "2 25544  51.6414 295.8524 0003435 262.6267 204.2868 15.54005638121106"
...> )
iex> el.catalog_number
"25544"
iex> el.inclination_deg
51.6414

parse_file(text, opts \\ [])

@spec parse_file(
  String.t(),
  keyword()
) ::
  {:ok,
   %{
     satellites: [file_satellite()],
     rejected: [rejected_record()],
     skipped: non_neg_integer()
   }}
  | {:error, {:invalid_field, :policy, term()}}

Parse a multi-record TLE file (CelesTrak / Space-Track style).

Accepts the common variants in a single pass: bare two-line element sets, three-line sets (a name line followed by lines 1 and 2), and CelesTrak 0 NAME name lines. Blank lines, CRLF endings, and surrounding whitespace are tolerated.

Returns {:ok, %{satellites: satellites, rejected: rejected, skipped: n}}. Each satellite is a map with name, tle, line_number (the one-based line of its line 1) and checksum_warnings. Each tle is a fully populated %Sidereon.Elements{} (with object_name set to the record's name, or nil for a bare two-line set) ready for Sidereon.propagate/2, Sidereon.look_angle/3, and friends. name is the empty string for a bare two-line record. rejected lists every other non-blank line in file order, each with its line number, its name line and the reason (see record_issue/0); one bad record never discards the others. skipped is the length of rejected, so an empty file (satellites: [], skipped: 0) is distinguishable from a fully corrupt one.

Options

  • :policy - :strict (default) or :lenient; under :strict a record whose checksum digit disagrees, or whose column 69 is not a digit, is rejected, and under :lenient it is kept and each finding is listed in its checksum_warnings.

Examples

iex> text = """
...> ISS (ZARYA)
...> 1 25544U 98067A   18184.80969102  .00001614  00000-0  31745-4 0  9993
...> 2 25544  51.6414 295.8524 0003435 262.6267 204.2868 15.54005638121106
...> """
iex> {:ok, %{satellites: [sat], skipped: 0}} = Sidereon.Format.TLE.parse_file(text)
iex> sat.name
"ISS (ZARYA)"
iex> sat.tle.catalog_number
"25544"

parse_with_warnings(longstr1, longstr2, opts \\ [])

@spec parse_with_warnings(String.t(), String.t(), keyword()) ::
  {:ok, Sidereon.Elements.t(), [checksum_warning()]}
  | {:error, Sidereon.CCSDS.Error.tle() | {:invalid_field, :policy, term()}}

Parse a two-line element set, returning the checksum warnings the policy accepted alongside the elements instead of logging them.

Returns {:ok, elements, checksum_warnings} or {:error, reason}. Takes the same options as parse/3.