Current section
Files
Jump to
Current section
Files
lib/sidereon/eclipse.ex
defmodule Sidereon.Eclipse do
@moduledoc """
Earth shadow (eclipse) prediction for satellites.
Determines whether a satellite is in full sunlight, penumbra, or umbra
using a conical shadow model. This is critical for satellite power budgets,
thermal analysis, and optical visibility.
## Shadow Model
Uses the conical shadow model which computes the penumbra and umbra cones
cast by Earth given the Sun's position. The satellite's position relative
to these cones determines its illumination state.
## Example
# Direct usage with pre-computed positions
status = Sidereon.Eclipse.status(satellite_gcrs_position, sun_position_from_earth)
# => :sunlit | :penumbra | :umbra
# Convenience: propagate + transform + check in one call
{:ok, status} = Sidereon.Eclipse.check(tle, datetime, ephemeris)
"""
alias Sidereon.NIF
defmodule EarthShadowModel do
@moduledoc """
Earth shape model used by eclipse shadow calculations.
"""
@typedoc "Supported Earth shadow geometry models."
@type t :: :spherical | :wgs84_oblate | String.t()
end
@doc """
Determine the eclipse status of a satellite.
## Parameters
* `sat_pos` - satellite GCRS position `{x, y, z}` in km
* `sun_pos` - Sun position relative to Earth `{x, y, z}` in km
(i.e., the vector from Earth center to the Sun)
## Returns
* `:sunlit` - satellite is in full sunlight
* `:penumbra` - satellite is partially shadowed
* `:umbra` - satellite is in full shadow
"""
@spec status(
{float(), float(), float()},
{float(), float(), float()}
) :: :sunlit | :penumbra | :umbra
def status({_x, _y, _z} = sat_pos, {_sx, _sy, _sz} = sun_pos), do: NIF.eclipse_status(sat_pos, sun_pos)
@doc """
Compute the shadow fraction for a satellite.
Returns a value from `0.0` (full sunlight) to `1.0` (full umbra).
Values between 0 and 1 indicate partial shadow (penumbra).
## Parameters
* `sat_pos` - satellite GCRS position `{x, y, z}` in km
* `sun_pos` - Sun position relative to Earth `{x, y, z}` in km
(vector from Earth center to the Sun)
"""
@spec shadow_fraction(
{float(), float(), float()},
{float(), float(), float()}
) :: float()
def shadow_fraction({_x, _y, _z} = sat_pos, {_sx, _sy, _sz} = sun_pos),
do: NIF.eclipse_shadow_fraction(sat_pos, sun_pos)
@doc """
Compute the shadow fraction with an explicit Earth shadow model.
"""
@spec shadow_fraction_with_model(
{float(), float(), float()},
{float(), float(), float()},
EarthShadowModel.t()
) :: float()
def shadow_fraction_with_model({_x, _y, _z} = sat_pos, {_sx, _sy, _sz} = sun_pos, model) do
NIF.eclipse_shadow_fraction_with_model(sat_pos, sun_pos, shadow_model(model))
end
@doc """
Determine eclipse status with an explicit Earth shadow model.
"""
@spec status_with_model(
{float(), float(), float()},
{float(), float(), float()},
EarthShadowModel.t()
) :: :sunlit | :penumbra | :umbra
def status_with_model({_x, _y, _z} = sat_pos, {_sx, _sy, _sz} = sun_pos, model) do
NIF.eclipse_status_with_model(sat_pos, sun_pos, shadow_model(model))
end
@doc """
Convenience function: propagate a TLE, transform to GCRS, fetch the Sun
position from an ephemeris, and return the eclipse status.
## Parameters
* `tle` - parsed `%Sidereon.Elements{}` struct
* `datetime` - `DateTime.t()` observation time
* `ephemeris` - loaded `%Sidereon.Ephemeris{}` handle
## Returns
* `:sunlit`, `:penumbra`, or `:umbra`
## Example
{:ok, tle} = Sidereon.parse_tle(line1, line2)
{:ok, eph} = Sidereon.Ephemeris.load("de421.bsp")
{:ok, status} = Sidereon.Eclipse.check(tle, ~U[2024-06-21 12:00:00Z], eph)
"""
@spec check(Sidereon.Elements.t(), DateTime.t(), Sidereon.Ephemeris.t()) ::
{:ok, :sunlit | :penumbra | :umbra} | {:error, term()}
def check(%Sidereon.Elements{} = tle, %DateTime{} = datetime, %Sidereon.Ephemeris{} = ephemeris) do
with {:ok, teme} <- Sidereon.SGP4.propagate(tle, datetime),
{:ok, sun_pos} <- Sidereon.Ephemeris.position(ephemeris, :sun, :earth, datetime) do
gcrs = Sidereon.Coordinates.teme_to_gcrs(teme, datetime)
{:ok, status(gcrs.position, sun_pos)}
end
end
defp shadow_model(:spherical), do: "spherical"
defp shadow_model(:wgs84_oblate), do: "wgs84_oblate"
defp shadow_model(value) when is_binary(value), do: value
end