Skip to content

Ephemeris

Orbital position calculation for satellites.

EphemerisCalculator

EphemerisCalculator(
    facility: Facility, datetimes: NDArray[object_]
)

Bases: ABC

Computes satellite positions relative to a ground facility.

Implementations use a specific physics engine (e.g. Skyfield/SGP4) to calculate satellite visibility windows and trajectories.

Initializes the calculator.

Parameters:

Name Type Description Default
facility Facility

The location observing the satellites.

required
datetimes NDArray[object_]

A sorted NumPy array of UTC datetime objects representing the simulation master grid.

required
Source code in src/sopp/ephemeris/base.py
@abstractmethod
def __init__(
    self,
    facility: Facility,
    datetimes: npt.NDArray[np.object_],  # datetime objects
):
    """
    Initializes the calculator.

    Args:
        facility: The location observing the satellites.
        datetimes: A sorted NumPy array of UTC datetime objects representing
                   the simulation master grid.
    """
    pass

Functions

calculate_visibility_windows abstractmethod

calculate_visibility_windows(
    satellite: Satellite,
    min_altitude: float,
    start_time: datetime,
    end_time: datetime,
) -> list[TimeWindow]

Calculates TimeWindows for Rise/Set events (where alt > min_alt).

Source code in src/sopp/ephemeris/base.py
@abstractmethod
def calculate_visibility_windows(
    self,
    satellite: Satellite,
    min_altitude: float,
    start_time: datetime,
    end_time: datetime,
) -> list[TimeWindow]:
    """
    Calculates TimeWindows for Rise/Set events (where alt > min_alt).
    """
    pass

    pass

calculate_trajectory abstractmethod

calculate_trajectory(
    satellite: Satellite, start: datetime, end: datetime
) -> SatelliteTrajectory

Calculates the continuous path for a single time window (Vector).

Source code in src/sopp/ephemeris/base.py
@abstractmethod
def calculate_trajectory(
    self, satellite: Satellite, start: datetime, end: datetime
) -> SatelliteTrajectory:
    """
    Calculates the continuous path for a single time window (Vector).
    """
    pass

calculate_trajectories abstractmethod

calculate_trajectories(
    satellite: Satellite, windows: list[TimeWindow]
) -> list[SatelliteTrajectory]

Calculates paths for multiple disjoint windows in one batch operation.

Source code in src/sopp/ephemeris/base.py
@abstractmethod
def calculate_trajectories(
    self, satellite: Satellite, windows: list[TimeWindow]
) -> list[SatelliteTrajectory]:
    """
    Calculates paths for multiple disjoint windows in one batch operation.
    """
    pass

calculate_position abstractmethod

calculate_position(
    satellite: Satellite, time: datetime
) -> Position

Calculates the position for a specific instant (Scalar).

Source code in src/sopp/ephemeris/base.py
@abstractmethod
def calculate_position(self, satellite: Satellite, time: datetime) -> Position:
    """
    Calculates the position for a specific instant (Scalar).
    """
    pass

SkyfieldEphemerisCalculator

SkyfieldEphemerisCalculator(
    facility: Facility, datetimes: list[datetime] | ndarray
)

Bases: EphemerisCalculator

Ephemeris calculator using Skyfield and SGP4 propagation.

Pre-computes Earth rotation matrices on a master time grid and injects cached values into subset time objects to avoid redundant calculations across multiple satellites.

Source code in src/sopp/ephemeris/skyfield.py
def __init__(self, facility: Facility, datetimes: list[datetime] | np.ndarray):
    self._facility = facility
    self._facility_latlon = self._calculate_facility_latlon()

    if isinstance(datetimes, np.ndarray):
        self._datetimes = np.array(datetimes, dtype=object, copy=True)
    else:
        self._datetimes = np.array(datetimes, dtype=object)

    # Sort so bisect works correctly
    if len(self._datetimes) > 0:
        self._datetimes.sort()

    self._grid_timescale = SKYFIELD_TIMESCALE.from_datetimes(self._datetimes)

    # Calcualte matrices
    if "M" not in self._grid_timescale.__dict__:
        _ = self._grid_timescale.M
    if "gast" not in self._grid_timescale.__dict__:
        _ = self._grid_timescale.gast
    if "gmst" not in self._grid_timescale.__dict__:
        _ = self._grid_timescale.gmst
    if "delta_t" not in self._grid_timescale.__dict__:
        _ = self._grid_timescale.delta_t

Functions

calculate_visibility_windows

calculate_visibility_windows(
    satellite: Satellite,
    min_altitude: float,
    start_time: datetime,
    end_time: datetime,
) -> list[TimeWindow]

Calculates Rise/Set/Culminate events.

Source code in src/sopp/ephemeris/skyfield.py
def calculate_visibility_windows(
    self,
    satellite: Satellite,
    min_altitude: float,
    start_time: datetime,
    end_time: datetime,
) -> list[TimeWindow]:
    """
    Calculates Rise/Set/Culminate events.
    """
    t0 = SKYFIELD_TIMESCALE.from_datetime(start_time)
    t1 = SKYFIELD_TIMESCALE.from_datetime(end_time)

    sat_skyfield = satellite.to_skyfield()

    times, events = sat_skyfield.find_events(
        self._facility_latlon, t0, t1, altitude_degrees=min_altitude
    )

    windows = []
    current_rise = None
    for t, event in zip(times, events, strict=True):
        if event == RISE_EVENT:
            current_rise = t.utc_datetime()
        elif event == SET_EVENT:
            if current_rise:
                windows.append(TimeWindow(current_rise, t.utc_datetime()))
                current_rise = None
            else:
                # Started mid-pass
                windows.append(TimeWindow(start_time, t.utc_datetime()))

    # Rose but didn't set
    if current_rise:
        windows.append(TimeWindow(current_rise, end_time))

    # Check for geostationary
    if not windows:
        # Check one point (start time)
        position = self.calculate_position(satellite, start_time)
        alt = position.altitude
        if alt >= min_altitude:
            windows.append(TimeWindow(start_time, end_time))

    return windows

calculate_position

calculate_position(
    satellite: Satellite, time: datetime
) -> Position

Retrieves the position for a single specific time.

Source code in src/sopp/ephemeris/skyfield.py
def calculate_position(self, satellite: Satellite, time: datetime) -> Position:
    """
    Retrieves the position for a single specific time.
    """

    idx = bisect.bisect_left(self._datetimes, time)
    target_t = None

    # Check if the index is valid and the time matches exactly
    if idx < len(self._datetimes) and self._datetimes[idx] == time:
        target_t = self._grid_timescale[idx]
        self._inject_earth_physics(target_t, idx)
    else:
        # The requested time is not in our grid.
        target_t = SKYFIELD_TIMESCALE.from_datetime(time)

    sat_skyfield = satellite.to_skyfield()
    difference = sat_skyfield - self._facility_latlon

    topocentric = difference.at(target_t)
    alt, az, dist = topocentric.altaz()

    return Position(altitude=alt.degrees, azimuth=az.degrees, distance_km=dist.km)