Sidereon.Constellation (Sidereon v3.0.0)

Copy Markdown View Source

Manage and propagate satellite constellations.

Build a constellation from parsed TLEs and propagate all satellites to a given time. Useful for visibility computations over a shared fleet.

Examples

constellation = Sidereon.Constellation.from_tles("custom", tles)
constellation.count

# Propagate all satellites to now
positions = Sidereon.Constellation.propagate_all(constellation, DateTime.utc_now())
Enum.each(positions, fn {norad_id, pos} ->
  IO.puts("#{norad_id}: #{inspect(pos)}")
end)

# Find visible satellites from a ground station
{:ok, visible} = Sidereon.Constellation.visible_from(constellation, station, datetime)

Summary

Functions

Create a constellation from a list of TLEs.

Compute WGS84 sub-satellite ground tracks for every satellite over a shared epoch grid.

Compute typed, index-preserving WGS84 ground-track outcomes for every satellite.

Compute topocentric look-angle arcs from a ground station for every satellite over a shared epoch grid.

Compute typed, index-preserving look-angle outcomes for every satellite.

Predict passes over a ground station for every satellite within a time window.

Predict typed, index-preserving per-satellite pass outcomes over a time window. Successful satellites retain an empty pass list; initialization and finder failures retain their complete typed cause at that satellite's fleet index.

Propagate all satellites to a given time.

Find satellites visible from a ground station at a given time.

Types

batch_error()

@type batch_error() ::
  {:invalid_satellites, invalid_satellites()}
  | {:invalid_option, term()}
  | {:invalid_field, atom(), term()}
  | :invalid_input
  | {:nif_error, String.t()}

detailed_fleet_pass()

@type detailed_fleet_pass() :: %{
  satellite_index: non_neg_integer(),
  value: %{catalog_number: String.t() | nil, passes: [Sidereon.Pass.t()]} | nil,
  error: detailed_pass_error() | nil
}

detailed_ground_track()

@type detailed_ground_track() :: %{
  satellite_index: non_neg_integer(),
  value: [Sidereon.Geodetic.t()] | nil,
  error: Sidereon.Coverage.look_angle_error() | nil
}

detailed_look_arc()

@type detailed_look_arc() :: %{
  satellite_index: non_neg_integer(),
  value: [Sidereon.LookAngle.t()] | nil,
  error: Sidereon.Coverage.look_angle_error() | nil
}

detailed_pass_error()

@type detailed_pass_error() ::
  {:satellite_initialization, Sidereon.Coverage.sgp4_error()}
  | {:invalid_input, String.t(), String.t()}
  | {:ut1_outside_coverage, :before_coverage | :after_coverage}

fleet_pass()

@type fleet_pass() :: %{
  satellite_index: non_neg_integer(),
  catalog_number: String.t() | nil,
  pass: Sidereon.Pass.t()
}

invalid_satellites()

@type invalid_satellites() :: [{String.t() | nil, {:error, term()}}]

t()

@type t() :: %Sidereon.Constellation{
  count: non_neg_integer(),
  name: String.t(),
  satellites: [Sidereon.Elements.t()]
}

visible_error()

@type visible_error() ::
  {:invalid_satellites, invalid_satellites()} | {:nif_error, String.t()}

visible_satellite()

@type visible_satellite() :: %{
  catalog_number: String.t() | nil,
  elevation: float(),
  azimuth: float(),
  range_km: float(),
  position: {float(), float(), float()}
}

Functions

from_tles(name, tles)

@spec from_tles(String.t(), [Sidereon.Elements.t()]) :: t()

Create a constellation from a list of TLEs.

Examples

constellation = Sidereon.Constellation.from_tles("custom", tles)

ground_tracks(constellation, datetimes, opts \\ [])

@spec ground_tracks(t(), [DateTime.t()], keyword()) ::
  {:ok, [[Sidereon.Geodetic.t()]]} | {:error, batch_error()}

Compute WGS84 sub-satellite ground tracks for every satellite over a shared epoch grid.

Returns one track per satellite in fleet order: element i is a list of %Sidereon.Geodetic{} (latitude/longitude in degrees, ellipsoidal altitude in km) for satellites[i], one per datetime. Each point is reduced TEME -> GCRS -> ITRS -> WGS84 geodetic by the engine's transforms. A satellite whose element set fails SGP4 initialization yields an empty track, keeping the result index-aligned. This is the batched companion to Sidereon.ground_track/3.

Options

  • :opsmode - SGP4 operation mode, :afspc (default) or :improved.

Examples

times = for s <- 0..600//60, do: DateTime.add(DateTime.utc_now(), s, :second)
{:ok, tracks} = Sidereon.Constellation.ground_tracks(constellation, times)
hd(tracks) |> hd() |> Map.get(:latitude)

ground_tracks_detailed(constellation, datetimes, opts \\ [])

@spec ground_tracks_detailed(t(), [DateTime.t()], keyword()) ::
  {:ok, [detailed_ground_track()]} | {:error, batch_error()}

Compute typed, index-preserving WGS84 ground-track outcomes for every satellite.

look_angle_arcs(constellation, station, datetimes, opts \\ [])

@spec look_angle_arcs(t(), map(), [DateTime.t()], keyword()) ::
  {:ok, [[Sidereon.LookAngle.t()]]} | {:error, batch_error()}

Compute topocentric look-angle arcs from a ground station for every satellite over a shared epoch grid.

Returns one arc per satellite in fleet order (the constellation's satellites order): element i is a list of %Sidereon.LookAngle{} for satellites[i], one per datetime. A satellite whose element set fails SGP4 initialization yields an empty arc, so the result stays index-aligned with the constellation. This is the batched companion to Sidereon.look_angle/4.

Options

  • :opsmode - SGP4 operation mode, :afspc (default) or :improved. Each satellite is built with this opsmode, so the arcs are consistent with visible_from/4 and passes/5 computed under the same opsmode.

Examples

times = for s <- 0..600//60, do: DateTime.add(DateTime.utc_now(), s, :second)
{:ok, arcs} = Sidereon.Constellation.look_angle_arcs(constellation, station, times)
hd(arcs) |> hd() |> Map.get(:elevation)

look_angle_arcs_detailed(constellation, station, datetimes, opts \\ [])

@spec look_angle_arcs_detailed(t(), map(), [DateTime.t()], keyword()) ::
  {:ok, [detailed_look_arc()]} | {:error, batch_error()}

Compute typed, index-preserving look-angle outcomes for every satellite.

Every input satellite receives one outcome; a successful empty arc has a nil error, while initialization, propagation, and frame errors retain their typed Sidereon.Coverage.look_angle_error/0 value.

passes(constellation, station, start_dt, end_dt, opts \\ [])

@spec passes(t(), map(), DateTime.t(), DateTime.t(), keyword()) ::
  {:ok, [fleet_pass()]} | {:error, batch_error()}

Predict passes over a ground station for every satellite within a time window.

Returns a flat list of passes across the whole constellation, each tagged with the fleet-order :satellite_index and the :catalog_number of the satellite it belongs to:

{:ok, [%{
  satellite_index: non_neg_integer(),
  catalog_number: String.t() | nil,
  pass: %Sidereon.Pass{}
}]}

Passes are emitted satellite by satellite in fleet order, each satellite's passes ordered by rise time. A satellite whose element set fails SGP4 initialization contributes no passes (its fleet index is still consumed, so indices match the constellation order). This is the constellation companion to Sidereon.Passes.predict/5.

Options

  • :min_elevation - minimum peak elevation in degrees to keep a pass (default 0.0); like Sidereon.Passes.predict/5, rise/set always reference the 0-degree horizon
  • :step_seconds - coarse propagation step in seconds (default 60)
  • :opsmode - SGP4 operation mode, :afspc (default) or :improved

Examples

start_dt = DateTime.utc_now()
end_dt = DateTime.add(start_dt, 86_400, :second)
{:ok, passes} = Sidereon.Constellation.passes(constellation, station, start_dt, end_dt)
hd(passes).satellite_index

passes_detailed(constellation, station, start_dt, end_dt, opts \\ [])

@spec passes_detailed(t(), map(), DateTime.t(), DateTime.t(), keyword()) ::
  {:ok, [detailed_fleet_pass()]} | {:error, batch_error()}

Predict typed, index-preserving per-satellite pass outcomes over a time window. Successful satellites retain an empty pass list; initialization and finder failures retain their complete typed cause at that satellite's fleet index.

propagate_all(constellation, datetime)

@spec propagate_all(t(), DateTime.t()) :: [
  {String.t() | nil, {:ok, Sidereon.TemeState.t()} | {:error, term()}}
]

Propagate all satellites to a given time.

Returns a list of {catalog_number, {:ok, teme_state}} or {catalog_number, {:error, reason}} tuples.

Examples

results = Sidereon.Constellation.propagate_all(constellation, ~U[2024-07-04 00:00:00Z])
for {id, {:ok, teme}} <- results do
  IO.puts("#{id}: #{inspect(teme.position)}")
end

visible_from(constellation, station, datetime, opts \\ [])

@spec visible_from(t(), map(), DateTime.t(), keyword()) ::
  {:ok, [visible_satellite()]} | {:error, visible_error()}

Find satellites visible from a ground station at a given time.

Returns satellites above min_elevation degrees, sorted by elevation (highest first).

Options

  • :min_elevation - minimum elevation in degrees (default: 10.0)
  • :opsmode - SGP4 operation mode, :afspc (default) or :improved. Each satellite is built with this opsmode, so visibility is consistent with the look angle and passes computed under the same opsmode.

Examples

{:ok, visible} = Sidereon.Constellation.visible_from(constellation, station, datetime)
for sat <- visible do
  IO.puts("#{sat.catalog_number}: el=#{sat.elevation}° range=#{sat.range_km} km")
end