Skip to content

Models

Data structures used throughout SOPP.

Core Types

Position dataclass

Position(
    altitude: float,
    azimuth: float,
    distance_km: float | None = None,
)

A position in the local horizontal coordinate system.

Attributes:

Name Type Description
altitude float

Elevation angle in degrees. 0 is the horizon, 90 is zenith. Negative values mean the object is below the horizon.

azimuth float

Azimuth angle in degrees, measured clockwise from geographic north (0) through east (90), south (180), and west (270).

distance_km float | None

Slant range to the object in kilometers, or None.

TimeWindow dataclass

TimeWindow(begin: datetime, end: datetime)

A time interval defined by a start and end time (UTC).

Attributes:

Name Type Description
begin datetime

Start of the window.

end datetime

End of the window.

Coordinates dataclass

Coordinates(latitude: float, longitude: float)

Geographic coordinates in decimal degrees.

Attributes:

Name Type Description
latitude float

Latitude in decimal degrees (positive north).

longitude float

Longitude in decimal degrees (positive east).

FrequencyRange dataclass

FrequencyRange(
    frequency: float,
    bandwidth: float,
    status: str | None = None,
)

A frequency band defined by center frequency and bandwidth.

Used for both telescope observations and satellite transmissions.

Attributes:

Name Type Description
frequency float

Center frequency in MHz.

bandwidth float

Total bandwidth in MHz.

status str | None

Optional metadata (e.g. 'active', 'inactive').

Functions

overlaps

overlaps(other: FrequencyRange) -> bool

Return True if this frequency range overlaps with another.

Source code in src/sopp/models/core.py
def overlaps(self, other: "FrequencyRange") -> bool:
    """Return True if this frequency range overlaps with another."""
    return (self.low_mhz < other.high_mhz) and (self.high_mhz > other.low_mhz)

Ground Station

Facility dataclass

Facility(
    coordinates: Coordinates,
    receiver: Receiver = Receiver(),
    elevation: float = 0,
    name: str | None = "Unnamed Facility",
)

The Facility data class contains the observation parameters of the facility.

Attributes:

Name Type Description
coordinates Coordinates

Location of RA facility.

receiver Receiver

Receive-side antenna characteristics. Defaults to Tier 0 with beamwidth=3.

elevation float

Ground elevation of the telescope in meters. Defaults to 0.

name str | None

Name of the facility. Defaults to 'Unnamed Facility'.

Receiver dataclass

Receiver(
    beamwidth: float = 3.0,
    peak_gain_dbi: float | None = None,
    antenna_pattern: AntennaPattern | None = None,
)

Receive-side antenna characteristics of a facility.

Supports three tiers of fidelity:

Tier 0 (geometric): Set only beamwidth for binary in/out-of-beam detection. Tier 1 (simple link budget): Set peak_gain_dbi for worst-case constant gain. Tier 1.5+ (detailed): Set antenna_pattern for angle-dependent gain lookup.

Attributes:

Name Type Description
beamwidth float

Beamwidth of the telescope in degrees.

peak_gain_dbi float | None

Peak antenna gain in dBi (Tier 1). Defaults to None.

antenna_pattern AntennaPattern | None

Full antenna gain pattern (Tier 1.5+). Defaults to None.

AntennaTrajectory dataclass

AntennaTrajectory(
    times: NDArray[object_],
    azimuth: NDArray[float64],
    altitude: NDArray[float64],
)

Time series of antenna azimuth/altitude positions.

Attributes:

Name Type Description
times NDArray[object_]

1D array of datetime objects (UTC).

azimuth NDArray[float64]

1D array of azimuth angles in degrees.

altitude NDArray[float64]

1D array of elevation angles in degrees.

Functions

get_state_at

get_state_at(
    query_times: ndarray,
) -> tuple[ndarray, ndarray]

Interpolate antenna az/alt at the given times.

Parameters:

Name Type Description Default
query_times ndarray

Array of datetime objects to interpolate at.

required

Returns:

Type Description
tuple[ndarray, ndarray]

Tuple of (azimuth, altitude) arrays in degrees.

Source code in src/sopp/models/ground/trajectory.py
def get_state_at(self, query_times: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """Interpolate antenna az/alt at the given times.

    Args:
        query_times: Array of datetime objects to interpolate at.

    Returns:
        Tuple of (azimuth, altitude) arrays in degrees.
    """
    # Convert datetimes to float timestamps for interpolation
    query_timestamps = np.array([t.timestamp() for t in query_times])

    interp_az_rad = np.interp(
        query_timestamps, self._ant_timestamps, self._unwrapped_az_rad
    )
    # Convert back to degrees and normalize
    interp_az = np.degrees(interp_az_rad) % 360.0

    interp_alt = np.interp(query_timestamps, self._ant_timestamps, self.altitude)

    return interp_az, interp_alt

ObservationTarget dataclass

ObservationTarget(declination: str, right_ascension: str)

A celestial target specified by equatorial coordinates.

Attributes:

Name Type Description
declination str

Declination string (e.g. '7d24m25.426s').

right_ascension str

Right ascension string (e.g. '5h55m10.3s').

Antenna Configuration

StaticPointingConfig dataclass

StaticPointingConfig(position: Position)

Fixed azimuth/elevation pointing for the entire observation.

CelestialTrackingConfig dataclass

CelestialTrackingConfig(target: ObservationTarget)

Track a celestial object (RA/Dec) across the sky.

CustomTrajectoryConfig dataclass

CustomTrajectoryConfig(trajectory: AntennaTrajectory)

User-provided antenna trajectory with explicit az/alt at each time step.

Satellite

Satellite dataclass

Satellite(
    name: str,
    tle_information: TleInformation | None = None,
    frequency: list[FrequencyRange] = list(),
    transmitter: Transmitter | None = None,
)

A satellite with a name and optional orbital/frequency data.

Satellites loaded from TLE files will have tle_information populated. Satellites reconstructed from trajectory files may not.

Attributes

satellite_number property

satellite_number: int | None

NORAD catalog number, if available.

orbits_per_day property

orbits_per_day: float

Calculate orbits per day from mean motion. Requires TLE data.

Functions

to_skyfield

to_skyfield() -> EarthSatellite

Convert to a Skyfield EarthSatellite. Requires TLE data.

Source code in src/sopp/models/satellite/satellite.py
def to_skyfield(self) -> EarthSatellite:
    """Convert to a Skyfield EarthSatellite. Requires TLE data."""
    if self.tle_information is None:
        raise ValueError(
            f"Satellite '{self.name}' has no TLE data. "
            "Cannot convert to Skyfield without orbital parameters."
        )
    line1, line2 = self.tle_information.to_tle_lines()
    return EarthSatellite(line1=line1, line2=line2, name=self.name)

SatelliteTrajectory dataclass

SatelliteTrajectory(
    satellite: Satellite,
    times: NDArray[object_],
    azimuth: NDArray[float64],
    altitude: NDArray[float64],
    distance_km: NDArray[float64],
)

Represents the computed path (trajectory) of a satellite relative to a facility.

Attributes:

Name Type Description
satellite Satellite

The satellite object associated with this trajectory.

times ndarray

1D array of datetime objects representing time steps.

azimuth ndarray

1D array of azimuth angles in degrees.

altitude ndarray

1D array of elevation/altitude angles in degrees.

distance_km ndarray

1D array of distances to the satellite in kilometers.

Example

Save a single trajectory

trajectory.save("path/to/file.arrow")

Save multiple trajectories to one file

from sopp.io import save_trajectories save_trajectories(trajectories, "batch.arrow")

Load trajectories (always returns a list)

trajectories = SatelliteTrajectory.load("path/to/file.arrow")

Attributes

overhead_time property

overhead_time

A TimeWindow representing the duration that the satellite is tracked within this trajectory (e.g., entering and exiting view). Returns None if the trajectory contains no data.

peak_index cached property

peak_index: int

Index of the highest elevation point in the pass.

peak_elevation cached property

peak_elevation: float

Maximum elevation in degrees.

peak_time cached property

peak_time: datetime | None

Time at which the satellite reaches peak elevation.

duration_seconds cached property

duration_seconds: float

Total duration of the pass in seconds.

azimuth_rate cached property

azimuth_rate: NDArray[float64]

Azimuth angular rate in degrees/second.

Returns an array with one fewer element than the input arrays. Each value represents the rate between consecutive time steps.

altitude_rate cached property

altitude_rate: NDArray[float64]

Elevation angular rate in degrees/second.

Returns an array with one fewer element than the input arrays. Each value represents the rate between consecutive time steps.

is_complete property

is_complete: bool

True if the pass rose and set within the observation window.

A complete pass has its peak elevation roughly centered in time, meaning we captured the full rise-peak-set arc rather than catching just the beginning or tail end.

Functions

save

save(
    path: str | Path | None = None,
    *,
    directory: str | Path | None = None,
    format: TrajectoryFormat | None = None,
    observer_name: str | None = None,
    observer_lat: float | None = None,
    observer_lon: float | None = None,
) -> Path

Save this trajectory to a file.

Parameters:

Name Type Description Default
path str | Path | None

Output file path. If None, directory must be provided.

None
directory str | Path | None

Directory for auto-generated filename. Ignored if path is set.

None
format TrajectoryFormat | None

File format handler. Defaults to ArrowFormat.

None
observer_name str | None

Optional name of the observing facility.

None
observer_lat float | None

Optional latitude of the observer.

None
observer_lon float | None

Optional longitude of the observer.

None

Returns:

Type Description
Path

Path to the saved file.

Raises:

Type Description
ValueError

If neither path nor directory is provided.

Source code in src/sopp/models/satellite/trajectory.py
def save(
    self,
    path: str | Path | None = None,
    *,
    directory: str | Path | None = None,
    format: TrajectoryFormat | None = None,
    observer_name: str | None = None,
    observer_lat: float | None = None,
    observer_lon: float | None = None,
) -> Path:
    """Save this trajectory to a file.

    Args:
        path: Output file path. If None, directory must be provided.
        directory: Directory for auto-generated filename. Ignored if path is set.
        format: File format handler. Defaults to ArrowFormat.
        observer_name: Optional name of the observing facility.
        observer_lat: Optional latitude of the observer.
        observer_lon: Optional longitude of the observer.

    Returns:
        Path to the saved file.

    Raises:
        ValueError: If neither path nor directory is provided.
    """
    if path is None and directory is None:
        raise ValueError("Either 'path' or 'directory' must be provided")

    if format is None:
        from sopp.io.formats.arrow import ArrowFormat

        format = ArrowFormat()

    if path is not None:
        output_path = Path(path)
    else:
        output_path = Path(directory)

    return format.save(
        self,
        output_path,
        observer_name=observer_name,
        observer_lat=observer_lat,
        observer_lon=observer_lon,
    )

load classmethod

load(
    path: str | Path,
    *,
    format: TrajectoryFormat | None = None,
    time_range: tuple[datetime, datetime] | None = None,
) -> TrajectorySet

Load trajectories from a file.

Parameters:

Name Type Description Default
path str | Path

Path to the trajectory file.

required
format TrajectoryFormat | None

File format handler. Defaults to ArrowFormat.

None
time_range tuple[datetime, datetime] | None

Optional tuple of (start, end) to filter trajectory data.

None

Returns:

Type Description
TrajectorySet

TrajectorySet of loaded trajectories.

Source code in src/sopp/models/satellite/trajectory.py
@classmethod
def load(
    cls,
    path: str | Path,
    *,
    format: TrajectoryFormat | None = None,
    time_range: tuple[datetime, datetime] | None = None,
) -> TrajectorySet:
    """Load trajectories from a file.

    Args:
        path: Path to the trajectory file.
        format: File format handler. Defaults to ArrowFormat.
        time_range: Optional tuple of (start, end) to filter trajectory data.

    Returns:
        TrajectorySet of loaded trajectories.
    """
    if format is None:
        from sopp.io.formats.arrow import ArrowFormat

        format = ArrowFormat()

    return format.load(Path(path), time_range=time_range)

TrajectorySet

TrajectorySet(trajectories: list[SatelliteTrajectory])

An iterable, filterable collection of satellite trajectories.

Returned by Sopp.get_satellites_above_horizon() and related methods. Supports filtering, observation scheduling, and plotting.

Construct from a list of trajectories::

ts = TrajectorySet(trajectories)

Filter, select, and chain::

selected = ts.filter(min_el=25, complete_only=True).select(min_separation_min=14)
Source code in src/sopp/models/satellite/trajectory_set.py
def __init__(self, trajectories: list[SatelliteTrajectory]):
    self._trajectories = sorted(
        trajectories, key=lambda t: t.peak_time or datetime.min
    )

Functions

to_list

to_list() -> list[SatelliteTrajectory]

Return the underlying list of trajectories.

Source code in src/sopp/models/satellite/trajectory_set.py
def to_list(self) -> list[SatelliteTrajectory]:
    """Return the underlying list of trajectories."""
    return list(self._trajectories)

filter

filter(
    min_el: float | None = None,
    max_el: float | None = None,
    complete_only: bool = False,
    name: str | None = None,
    max_az_rate: float | None = None,
    max_el_rate: float | None = None,
) -> TrajectorySet

Return a filtered copy.

Parameters:

Name Type Description Default
min_el float | None

Minimum peak elevation in degrees.

None
max_el float | None

Maximum peak elevation in degrees.

None
complete_only bool

Only include complete rise-peak-set passes.

False
name str | None

Only include satellites whose name contains this string (case-insensitive).

None
max_az_rate float | None

Maximum azimuth rate in deg/sec (antenna slew limit).

None
max_el_rate float | None

Maximum elevation rate in deg/sec (antenna slew limit).

None
Source code in src/sopp/models/satellite/trajectory_set.py
def filter(
    self,
    min_el: float | None = None,
    max_el: float | None = None,
    complete_only: bool = False,
    name: str | None = None,
    max_az_rate: float | None = None,
    max_el_rate: float | None = None,
) -> TrajectorySet:
    """Return a filtered copy.

    Args:
        min_el: Minimum peak elevation in degrees.
        max_el: Maximum peak elevation in degrees.
        complete_only: Only include complete rise-peak-set passes.
        name: Only include satellites whose name contains this string (case-insensitive).
        max_az_rate: Maximum azimuth rate in deg/sec (antenna slew limit).
        max_el_rate: Maximum elevation rate in deg/sec (antenna slew limit).
    """
    result = self._trajectories
    if min_el is not None:
        result = [t for t in result if t.peak_elevation >= min_el]
    if max_el is not None:
        result = [t for t in result if t.peak_elevation <= max_el]
    if complete_only:
        result = [t for t in result if t.is_complete]
    if name is not None:
        result = [t for t in result if name.lower() in t.satellite.name.lower()]
    if max_az_rate is not None:
        result = [
            t
            for t in result
            if len(t.azimuth_rate) > 0
            and np.max(np.abs(t.azimuth_rate)) < max_az_rate
        ]
    if max_el_rate is not None:
        result = [
            t
            for t in result
            if len(t.altitude_rate) > 0
            and np.max(np.abs(t.altitude_rate)) < max_el_rate
        ]
    return TrajectorySet(result)

select

select(min_separation_min: float = 14) -> TrajectorySet

Select non-overlapping passes with minimum time separation.

Walks through passes in time order and picks each one that is far enough from the last selected pass. Apply filter() first to control which passes are candidates.

Parameters:

Name Type Description Default
min_separation_min float

Minimum minutes between selected pass peaks.

14
Source code in src/sopp/models/satellite/trajectory_set.py
def select(self, min_separation_min: float = 14) -> TrajectorySet:
    """Select non-overlapping passes with minimum time separation.

    Walks through passes in time order and picks each one that is
    far enough from the last selected pass. Apply ``filter()`` first
    to control which passes are candidates.

    Args:
        min_separation_min: Minimum minutes between selected pass peaks.
    """
    selected = []
    last_time = None
    sep = timedelta(minutes=min_separation_min)

    for t in self._trajectories:
        if t.peak_time is None:
            continue
        if last_time is not None and (t.peak_time - last_time) < sep:
            continue
        selected.append(t)
        last_time = t.peak_time

    return TrajectorySet(selected)

TleInformation dataclass

TleInformation(
    argument_of_perigee: float,
    drag_coefficient: float,
    eccentricity: float,
    epoch_days: float,
    inclination: float,
    mean_anomaly: float,
    mean_motion: MeanMotion,
    revolution_number: int,
    right_ascension_of_ascending_node: float,
    satellite_number: int,
    classification: str = "U",
    international_designator: InternationalDesignator
    | None = None,
)

Parsed orbital elements from a Two-Line Element set.

All angular values are in radians (SGP4 convention).

Transmitter dataclass

Transmitter(
    eirp_dbw: float | None = None,
    power_dbw: float | None = None,
    antenna_pattern: AntennaPattern | None = None,
)

RF transmission characteristics of a satellite.

Supports two modes of operation:

Tier 1 (simple): Set only eirp_dbw for a worst-case constant EIRP. This assumes main beam alignment and is useful for quick screening.

Tier 2 (detailed): Set power_dbw and antenna_pattern separately. This allows calculating angle-dependent EIRP based on where the receiver is relative to the satellite's antenna boresight.

Attributes:

Name Type Description
eirp_dbw float | None

Effective Isotropic Radiated Power in dBW (Tier 1). Combined P_t * G_t as a single scalar. Use this for simple worst-case estimates.

power_dbw float | None

Transmitter power in dBW, before antenna gain (Tier 2). Use with antenna_pattern for angle-dependent calculations.

antenna_pattern AntennaPattern | None

Satellite antenna gain pattern (Tier 2). If provided with power_dbw, enables angle-dependent EIRP.

Attributes

peak_eirp_dbw property

peak_eirp_dbw: float

Peak EIRP (at satellite boresight).

Functions

get_eirp_dbw

get_eirp_dbw(
    off_axis_deg: float | ndarray = 0.0,
) -> float | ndarray

Get EIRP at a given off-axis angle from the satellite's boresight.

For Tier 1 (eirp_dbw set): Returns constant EIRP regardless of angle. For Tier 2 (power_dbw + pattern): Returns P_t + G_t(angle).

Parameters:

Name Type Description Default
off_axis_deg float | ndarray

Angle from satellite antenna boresight in degrees. Can be scalar or array. Ignored for Tier 1.

0.0

Returns:

Type Description
float | ndarray

EIRP in dBW. Scalar if input is scalar, array if input is array.

Source code in src/sopp/models/satellite/transmitter.py
def get_eirp_dbw(
    self, off_axis_deg: float | np.ndarray = 0.0
) -> float | np.ndarray:
    """Get EIRP at a given off-axis angle from the satellite's boresight.

    For Tier 1 (eirp_dbw set): Returns constant EIRP regardless of angle.
    For Tier 2 (power_dbw + pattern): Returns P_t + G_t(angle).

    Args:
        off_axis_deg: Angle from satellite antenna boresight in degrees.
            Can be scalar or array. Ignored for Tier 1.

    Returns:
        EIRP in dBW. Scalar if input is scalar, array if input is array.
    """
    if self.eirp_dbw is not None:
        return self.eirp_dbw

    # Tier 2: angle-dependent EIRP (fields validated in __post_init__)
    g_t = self.antenna_pattern.get_gain(off_axis_deg)  # type: ignore[union-attr]
    return self.power_dbw + g_t  # type: ignore[operator]

Antenna Pattern

AntennaPattern dataclass

AntennaPattern(angles_deg: ndarray, gains_dbi: ndarray)

Antenna gain as a function of off-axis angle.

Represents a rotationally symmetric antenna pattern where gain depends only on the angular distance from boresight (the pointing direction).

Attributes:

Name Type Description
angles_deg ndarray

Off-axis angles in degrees, must start at 0 (boresight). Should be monotonically increasing.

gains_dbi ndarray

Corresponding gain values in dBi. First value is peak gain.

Attributes

peak_gain_dbi property

peak_gain_dbi: float

Peak gain at boresight (0 degrees off-axis).

Functions

get_gain

get_gain(off_axis_deg: float | ndarray) -> float | ndarray

Interpolate gain at given off-axis angle(s).

Parameters:

Name Type Description Default
off_axis_deg float | ndarray

Off-axis angle(s) in degrees. Can be scalar or array.

required

Returns:

Type Description
float | ndarray

Gain in dBi at the specified angle(s). Same shape as input.

Source code in src/sopp/models/antenna.py
def get_gain(self, off_axis_deg: float | np.ndarray) -> float | np.ndarray:
    """Interpolate gain at given off-axis angle(s).

    Args:
        off_axis_deg: Off-axis angle(s) in degrees. Can be scalar or array.

    Returns:
        Gain in dBi at the specified angle(s). Same shape as input.
    """
    return np.interp(off_axis_deg, self.angles_deg, self.gains_dbi)

Simulation

Reservation dataclass

Reservation(
    facility: Facility,
    time: TimeWindow,
    frequency: FrequencyRange | None = None,
)

A scheduled observation at a facility.

Attributes:

Name Type Description
facility Facility

The radio astronomy facility.

time TimeWindow

Time window of the observation.

frequency FrequencyRange | None

Frequency band being observed. Optional for horizon-only queries.

Configuration dataclass

Configuration(
    reservation: Reservation,
    satellites: list[Satellite],
    antenna_config: AntennaConfig | None = None,
    runtime_settings: RuntimeSettings = RuntimeSettings(),
)

Complete simulation configuration.

Combines the observation reservation, satellite list, antenna pointing mode, and runtime settings. Validates all fields on construction.

Attributes:

Name Type Description
reservation Reservation

Facility, time window, and frequency.

satellites list[Satellite]

List of satellites to analyze.

antenna_config AntennaConfig | None

How the antenna is pointed during the observation.

runtime_settings RuntimeSettings

Execution parameters (resolution, concurrency, etc.).

RuntimeSettings dataclass

RuntimeSettings(
    time_resolution_seconds: float = 1,
    concurrency_level: int = 1,
    min_altitude: float = 0.0,
)

Controls execution and precision of the simulation.

Attributes:

Name Type Description
time_resolution_seconds float

Step size of the simulation grid in seconds.

concurrency_level int

Number of parallel processes for satellite calculations.

min_altitude float

Minimum elevation angle in degrees for a satellite to be considered above the horizon.

Results

InterferenceResult dataclass

InterferenceResult(
    trajectory: SatelliteTrajectory,
    interference_level: ndarray | None = None,
    level_units: str | None = None,
    metadata: dict | None = None,
)

The output of any interference strategy.

Wraps a SatelliteTrajectory (the interfering segment) with optional quantitative interference data. All strategies produce this same structure, making them interchangeable.