Skip to main content

SOPP is an open-source tool for calculating satellite interference to radio astronomy observations.

Project description

S.O.P.P. - Satellite Orbit Prediction Processor

Quick Start Guide

Welcome to S.O.P.P., an open-source tool for calculating satellite interference in radio astronomy observations.

Introduction

The SOPP package assists astronomers in optimizing observation scheduling to mitigate radio interference from satellite sources. This is achieved by computing the positions of satellites relative to the observation facility and determining which of these satellites cause interference with the main beam during the observation.

The primary functionality offered by the package is accessed through the Sopp class. This class implements two methods:

  • get_satellites_crossing_main_beam
  • get_satellites_above_horizon

High-Level Overview

  1. Define Observation Characteristics:

    • Provide the necessary observation characteristics to the ConfigurationBuilder class:
      • Facility Location
      • Observation Time Window
      • Antenna Frequency and Bandwidth
      • Antenna observation path
      • Satellite TLE data
      • Satellite Filtering (optional)
      • Runtime settings
  2. Determine Satellite Interference:

    • Create an instance of Sopp using the Configuration built by the ConfigurationBuilder class.
    • Utilize the methods of the Sopp class to obtain position data of interfering satellites:
      • get_satellites_crossing_main_beam: Returns satellites that cross the main beam during observation.
      • get_satellites_above_horizon: Returns all satellites above the horizon during the observation.

Define Observation Characteristics

ConfigurationBuilder

The observation characteristics must be provided to the ConfigurationBuilder class. The configuration builder will then construct a fully prepared Configuration object used to determine satellite interference.

An example using ConfigurationBuilder:

from sopp.builder.configuration_builder import ConfigurationBuilder

configuration = (
    ConfigurationBuilder()
    .set_facility(
        latitude=40.8178049,
        longitude=-121.4695413,
        elevation=986,
        name='HCRO',
        beamwidth=3,
    )
    .set_frequency_range(
        bandwidth=10,
        frequency=135
    )
    .set_time_window(
        begin='2023-11-15T08:00:00.0',
        end='2023-11-15T08:30:00.0'
    )
    .set_observation_target(
        declination='7d24m25.426s',
        right_ascension='5h55m10.3s'
    )
    .set_runtime_settings(
        concurrency_level=8,
        time_continuity_resolution=1,
        min_altitude=5.0,
    )
    # Alternatively set all of the above settings from a config file
    #.set_from_config_file(config_file='./supplements/config.json')
    .set_satellites(tle_file='./supplements/satellites.tle')
    .build()
)

The ConfigurationBuilder must call the following methods with the necessary arguments:

  • set_facility()
  • set_frequency_range()
  • set_time_window()
  • set_observation_target()
  • set_satellites()
  • set_runtime_settings()

set_facility()

The set_facility() method specifies the latitude, longitude, elevation (in meters), name, bandwidth, beamwidth and frequency:

configuration.set_facility(
    latitude=40.8178049,
    longitude=-121.4695413,
    elevation=986,
    name='HCRO',
    beamwidth=3,
    bandwidth=10,
    frequency=135
)

The set_frequency_range() method specifies bandwidth, and frequency of the observation:

configuration.set_frequency_range(
    bandwidth=10,
    frequency=135
)

set_time_window()

The set_time_window() method defines the observation time window in UTC, specifying when the observation will take place. The date format follows the ISO 8601 datetime format: Y-m-dTH:M:S.f. The provided datetime string can include microseconds or not. It additionally accepts a time zone, for example, 2023-11-15T08:00:00-7:00. Alternatively, begin and end can be provided as datetimes. All times are converted to UTC.

configuration.set_time_window(
    begin='2023-11-15T08:00:00.0',
    end='2023-11-15T08:30:00.0'
)

set_observation_target()

The set_observation_target() method defines the target for observation. It has three options:

  1. Specify a target by providing its declination and right ascension:
configuration.set_observation_target(
    declination='7d24m25.426s',
    right_ascension='5h55m10.3s'
)
  1. Specify a static location to observe by providing an azimuth and altitude:
configuration.set_observation_target(
    azimuth=24.2,
    altitude=78.1
)
  1. Provide a custom antenna direction path (how to construct a custom path is explained later):
configuration.set_observation_target(
    custom_path=custom_path
)

set_satellites()

The set_satellites() method takes the file path of the satellite TLE data and an optional frequency file. The frequency data can be utilized to filter out satellites whose downlink frequency does not overlap with the observation frequency.

configuration.set_satellites(
    tle_file='path/to/satellites.tle',
    frequency_file='/path/to/frequency.csv' # optional
)

set_satellites_filter()

There is optionally the ability to filter the satellites list. To do this call the set_satellites_filter() method with a Filterer object. The Filterer object is explained in more detail in the 'Filtering Satellites' section.

configuration.set_satellites_filter(
    filterer=filterer
)

add_filter()

Alternatively to constructing a Filterer object you can simply call add_filter()

configuration.add_filter(filter_frequency(FrequencyRange(10, 10)))

set_runtime_settings()

The set_runtime_settings() method:

  • Specifies the time resolution for calculating satellite positions in seconds via the time_continuity_resolution parameter.
  • Specifies the concurrency_level parameter determines the number of parallel jobs during satellite position calculation, optimizing runtime speeds. This value should be not exceed the number of cores on the machine.
  • The min_altitude specifies the minimum altitude a satellite must be to be considered above the horizon. Useful for locations with obstructed horizons.
  • Runtime settings are optional, the defaults are: concurrency_level = 1, time_continuity_resolution = 1 and min_altitude = 0.0.
configuration.set_runtime_settings(
    concurrency_level=8,
    time_continuity_resolution=1,
    min_altitude=0.0,
)

build()

Finally, once all the required methods have been called use the build() method to obtain the Configuration object:

configuration = configuration.build()

set_from_config_file()

The set_from_config_file() method can be used to provide all of the observation characteristics via a JSON configuration file instead of being set programatically.

configuration = (
    ConfigurationBuilder()
    .set_from_config_file(config_file='./supplements/config.json')
    .set_satellites(tle_file='./supplements/satellites.tle')
    .build()
)

The JSON config file follows the following format:

{
  "facility": {
    "beamwidth": 3,
    "elevation": 986,
    "latitude": 40.8178049,
    "longitude": -121.4695413,
    "name": "HCRO"
  },
  "frequencyRange": {
    "bandwidth": 10,
    "frequency": 135
  },
  "observationTarget": {
    "declination": "-38d6m50.8s",
    "rightAscension": "4h42m"
  },
  "reservationWindow": {
    "startTimeUtc": "2023-09-27T12:00:00.000000",
    "endTimeUtc": "2023-09-27T13:00:00.000000"
  },
  "runtimeSettings": {
      "concurrency_level": 4,
      "time_continuity_resolution": 1,
      "min_altitude": 0.0
  }
}

Determine Satellite Interference

Sopp

The Sopp class utilizes the previously created configuration object to identify satellite interference. It is initialized with the Configuration obtained from ConfigurationBuilder.

from sopp.sopp import Sopp

sopp = Sopp(configuration=configuration)

Finally, obtain the position data of interfering satellites, run either:

  • get_satellites_crossing_main_beam: Returns satellites that cross the main beam during observation.
  • get_satellites_above_horizon: Returns all satellites that are above the horizon during the observation.
interference_events = sopp.get_satellites_crossing_main_beam()

The data is returned as a list of OverheadWindow, which is defined as:

class OverheadWindow:
    satellite: Satellite
    positions: List[PositionTime]

The Satellite class, containins details about the satellite and a list of PositionTime objects. The PositionTime dataclass specifies the satellite's position in altitude, azimuth and distance in km at a discrete point in time. All times are in UTC.

Filtering Satellites

The list of satellites can be filtered by using a Filterer object, adding filters to it and then passing the Filterer object to a ConfigurationBuilder. The user can define any filtering logic wanted, however a few built in filters are provided. If the filtering condition evaluates to True the Satellite will be included in the final list. If None is passed to any of the filters, no filtering for that specific filter will be applied. Alternatively to passing a Filterer object to the ConfigurationBuilder via set_satellites_filter, filters can simply be added with add_filter(filter_name_contains('STARLINK')).

The provided filters accessible from sopp.satellites_filter.filters include:

filter_frequency():

Parameters: - observation_frequency (FrequencyRange): The observation frequency range.

returns True if a satellite's downlink transmission frequency overlaps with the desired observation frequency. If there is no information on the satellite frequency, it will return True to err on the side of caution for potential interference. Requires a FrequencyRange object.

filter_name_regex():

Parameters: - regex (str): The regex to match for in the satellite names.

returns True if a given regex matches in the name of a Satellite.

Example: filter_name_regex('YAM|ZARYA') will return a list of satellites that contain YAM or ZARYA within their name.

filter_name_contains():

Parameters: - substring (str): The substring to check for in the satellite names.

returns True if a given substring is present in the name of a Satellite.

filter_name_does_not_contain:

Parameters: - substring (str): The substring to check for absence for in the satellite names.

returns True if a given substring is not present in the name of a Satellite.

filter_name_is():

Parameters: - substring (str): The substring to match for in the satellite names.

returns True if a given substring matches exactly the name of a Satellite.

filter_orbit_is(orbit_type):

Parameters: - orbit_type (str): The type of orbit ('leo', 'meo', or 'geo').

if orbit_type='leo' returns Low Earth Orbit (LEO) satellites based on their orbital period. The filter checks if the satellite's orbits per day is >= 5.0

if orbit_type='meo' returns Medium Earth Orbit (MEO) satellites based on their orbital period. The filter checks if the satellite's orbits per day is >= 1.5 and < 5.0

if orbit_type='geo' returns Geostationary Orbit (GEO) satellites based on their orbital period. The filter checks if the satellite's orbits per day is >= 0.85 and < 1.5

For example, to find all Satellites that are not Starlink, but are in LEO and that have overlapping downlink transmission frequency:

from sopp.satellites_filter.filterer import Filterer
from sopp.satellites_filter.filters import (
    filter_name_does_not_contain,
    filter_orbit_is,
    filter_frequency,
    filter_name_is,
)

filterer = (
    Filterer()
    .add_filter(filter_name_does_not_contain('STARLINK'))
    .add_filter(filter_orbit_is(orbit_type='leo'))
    .add_filter(filter_frequency())
    .add_filter(filter_name_is(None)) # this filter will do nothing
)

User defined filters can be defined as well. The add_filter method takes a lambda. For example, if the user would prefer to define LEO satellites differently than the provided filtering function:

filterer = (
    Filterer()
    .add_filter(lambda satellite: satellite.orbital_period <= 100.0)
)

Using TleFetcher to Obtain TLE File

A TleFetcher class exists that will automatically fetch the latest TLEs from Celestrak or SpaceTrack:

from sopp.tle_fetcher.tle_fetcher_celestrak import TleFetcherCelestrak

fetcher = TleFetcherCelestrak(tle_file_path='path/to/save/satellites.tle')
fetcher.fetch_tles()

SpaceTrack is called identically, however you must set the environment variable IDENTITY with your username and PASSWORD with your password:

from sopp.tle_fetcher.tle_fetcher_spacetrack import TleFetcherSpacetrack

fetcher = TleFetcherSpacetrack(tle_file_path='path/to/save/satellites.tle')
fetcher.fetch_tles()

Providing Custom Path for Observation

Instead of specifying an observation target or static observation with altitude and azimuth a custom path can be provided as a list of PositionTime objects.

custom_path = [
    PositionTime(
        position=Position(altitude=.0, azimuth=.1),
        time=datetime(year=2023, month=3, day=30, hour=10, minute=1, tzinfo=pytz.UTC)
    ),
    PositionTime(
        position=Position(altitude=.1, azimuth=.2),
        time=datetime(year=2023, month=3, day=30, hour=10, minute=2, tzinfo=pytz.UTC)
    ),
    PositionTime(
        position=Position(altitude=.2, azimuth=.2),
        time=datetime(year=2023, month=3, day=30, hour=10, minute=3, tzinfo=pytz.UTC)
    ),
]

Example Code

from sopp.sopp import Sopp
from sopp.builder.configuration_builder import ConfigurationBuilder
from sopp.satellites_filter.filters import (
    filter_name_does_not_contain,
    filter_orbit_is,
)


def main():
    configuration = (
        ConfigurationBuilder()
        .set_facility(
            latitude=40.8178049,
            longitude=-121.4695413,
            elevation=986,
            name='HCRO',
            beamwidth=3,
        )
        .set_frequency_range(
            bandwidth=10,
            frequency=135
        )
        .set_time_window(
            begin='2024-01-18T08:00:00.0',
            end='2024-01-18T08:30:00.0'
        )
        .set_observation_target(
            declination='7d24m25.426s',
            right_ascension='5h55m10.3s'
        )
        .set_runtime_settings(
            concurrency_level=8,
            time_continuity_resolution=1
        )
        # Alternatively set all of the above settings from a config file
        #.set_from_config_file(config_file='./supplements/config.json')
        .set_satellites(tle_file='./supplements/satellites.tle')
        .add_filter(filter_name_does_not_contain('STARLINK'))
        .add_filter(filter_orbit_is(orbit_type='leo'))
        .build()
    )

    # Display configuration
    print('\nFinding satellite interference events for:\n')
    print(f'Facility: {configuration.reservation.facility.name}')
    print(f'Location: {configuration.reservation.facility.coordinates} at elevation '
          f'{configuration.reservation.facility.elevation}')
    print(f'Reservation start time: {configuration.reservation.time.begin}')
    print(f'Reservation end time: {configuration.reservation.time.end}')
    print(f'Observation frequency: {configuration.reservation.frequency.frequency} MHz')

    # Determine Satellite Interference
    sopp = Sopp(configuration=configuration)
    interference_events = sopp.get_satellites_crossing_main_beam()

    print('\n==============================================================\n')
    print(f'There are {len(interference_events)} satellite interference\n'
          f'events during the reservation\n')
    print('==============================================================\n')

    for i, window in enumerate(interference_events, start=1):
        max_alt = max(window.positions, key=lambda pt: pt.position.altitude)

        print(f'Satellite interference event #{i}:')
        print(f'Satellite: {window.satellite.name}')
        print(f'Satellite enters view: {window.overhead_time.begin} at '
              f'{window.positions[0].position.azimuth:.2f} '
              f'Distance: {window.positions[0].position.distance_km:.2f} km')
        print(f'Satellite leaves view: {window.overhead_time.end} at '
              f'{window.positions[-1].position.azimuth:.2f} '
              f'Distance: {window.positions[-1].position.distance_km:.2f} km')
        print(f'Satellite maximum altitude: {max_alt.position.altitude:.2f}')
        print('__________________________________________________\n')


if __name__ == '__main__':
    main()

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sopp-0.8.2.tar.gz (36.5 kB view hashes)

Uploaded Source

Built Distribution

sopp-0.8.2-py3-none-any.whl (57.0 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page