Satellite Ground Contacts

Compute communications contact intervals of “LandSat 7” saetllite over a single day

[1]:
import satkit as sk
import numpy as np
import plotly.express as px
from datetime import datetime, timedelta

# The TLE for landsat-7
tle_lines = [
    "0 LANDSAT-7",
    "1 25682U 99020A   24099.90566066  .00000551  00000-0  12253-3 0  9992",
    "2 25682  97.8952 129.9471 0001421 108.5441  14.5268 14.60548156329087"
]
landsat7 = sk.TLE.from_lines(tle_lines)

# The mininum elevation for a contact
min_elevation_deg = 5


# The date to compute ground contacts: April 9, 2024
date = datetime(2024, 4, 9)
# Any array of times representing every second of the day
time_array = np.array([date + timedelta(seconds=x) for x in range(86400)])

# Get satellite positions in TEME frame (pseudo-inertial) via SGP4
pTEME, _vTEME = sk.sgp4(landsat7, time_array)

# Get ITRF coordinates (Earth-Fixed) by rotating the position in the TEME frame
# to ITRF frame using the frametransform module
pITRF = np.array([q*x for q,x in zip(sk.frametransform.qteme2itrf(time_array), pTEME)])

# Setup some ground stations
ground_stations = [
    {'name': 'Svalbard', 'lat': 78.2232, 'lon': 15.6267, 'alt': 0},
    {'name': 'Alice Springs', 'lat': -23.6980, 'lon': 133.8807, 'alt': 0},
    {'name': 'Sioux Falls', 'lat': 43.5446, 'lon': -96.7311, 'alt': 0},
]

[2]:
def calc_contacts(ground_station, pITRF, time_array):
    """
    Compute contact times for a single ground station given satellite position in Earth-fixed frame
    """

    # Create an "itrfcoord" object for the ground statoin
    coord = sk.itrfcoord(latitude_deg=ground_station['lat'], longitude_deg=ground_station['lon'], altitude_m=ground_station['alt'])

    # Get the North-East-Down coordinates of the satellite relative to the ground station
    # at all times by taking the difference between the satellite position and the ground
    # coordinated, then rotating to the "North-East-Down" frame relative to the ground station
    pNED = np.array([coord.qned2itrf.conj * (x - coord.vector) for x in pITRF])

    # Normalize the NED coordinates
    pNED_hat = pNED / np.linalg.norm(pNED, axis=1)[:, None]

    # Find the elevation from the ground station at all times
    # This is the arcsign of the "up" portion of the NED-hat vetor
    elevation_deg = np.degrees(np.arcsin(-pNED_hat[:,2]))

    # We can see ground station when elevation is greater than min_elevation_deg
    inview_idx = np.argwhere(elevation_deg > min_elevation_deg).flatten().astype(int)

    # split indices into groups of consecutive indices
    # This indicates contiguous contacts
    inview_idx = np.split(inview_idx, np.where(np.diff(inview_idx) != 1)[0]+1)

    def get_single_contacts(inview_idx):
        for cidx in inview_idx:
            # cidx are indices to the time array for this contact

            # the North-East-Down position of the satellite relative to
            # ground station over the single contact
            cpNED = pNED[cidx,:]

            # Compute the range in meters
            range = np.linalg.norm(cpNED, axis=1)

            # elevation in degrees over the contact
            contact_elevation_deg = elevation_deg[cidx]

            # Heading clockwise from North is arctangent of east/north'
            heading_deg = np.degrees(np.arctan2(cpNED[:,1], cpNED[:,0]))

            # Yield a dictionary describing the results
            yield {
                'groundstation': ground_station['name'],
                'timearr': time_array[cidx],
                'range_km': range*1.0e-3,
                'elevation_deg': contact_elevation_deg,
                'heading_deg': heading_deg,
                'start': time_array[cidx[0]],
                'end': time_array[cidx[-1]],
                'max_elevation_deg': np.max(contact_elevation_deg),
                'duration': time_array[cidx[-1]] - time_array[cidx[0]]
            }
    return list(get_single_contacts(inview_idx))

# Calculate all the contacts
contacts = [calc_contacts(g, pITRF, time_array) for g in ground_stations]

# Flatten contacts into 1D list
contacts = [item for sublist in contacts for item in sublist]
[3]:
# Convert to pandas dataframe for nice table display

import pandas as pd

data = pd.DataFrame(contacts)
data.sort_values(by='start', inplace=True)
data.reset_index(drop=True, inplace=True)
# Get nicer column names for display
data.rename(columns={"max_elevation_deg": "Max Elevation (deg)",
                     "duration": "Duration (s)",
                     "start": "Start (UTC)",
                     "end": "End (UTC)",
                     "groundstation": "Ground Station"}, inplace=True)
data.style \
    .hide(subset=["timearr", "range_km", "elevation_deg", "heading_deg"], axis=1) \
    .format({"Max Elevation (deg)": "{:.1f}",
             "Start (UTC)": lambda x: x.strftime('%H:%M:%S'),
             "End (UTC)": lambda x: x.strftime('%H:%M:%S'),
               "Duration (s)": lambda x: x.seconds  })


[3]:
  Ground Station Start (UTC) End (UTC) Max Elevation (deg) Duration (s)
0 Sioux Falls 00:35:13 00:45:45 32.6 632
1 Svalbard 00:50:29 00:55:06 7.1 277
2 Sioux Falls 02:12:52 02:23:13 27.6 621
3 Svalbard 02:29:38 02:36:04 9.5 386
4 Svalbard 04:08:08 04:16:54 15.6 526
5 Svalbard 05:46:24 05:56:51 27.4 627
6 Svalbard 07:24:29 07:35:47 50.7 678
7 Svalbard 09:02:21 09:13:52 88.4 691
8 Alice Springs 10:08:08 10:18:52 33.7 644
9 Svalbard 10:39:59 10:51:21 63.4 682
10 Alice Springs 11:46:26 11:54:59 15.7 513
11 Svalbard 12:17:23 12:28:38 54.7 675
12 Sioux Falls 12:34:01 12:37:54 6.5 233
13 Svalbard 13:54:40 14:06:01 62.1 681
14 Sioux Falls 14:08:38 14:20:01 69.3 683
15 Svalbard 15:32:08 15:43:38 88.8 690
16 Sioux Falls 15:47:13 15:55:26 14.7 493
17 Svalbard 17:10:09 17:21:29 53.1 680
18 Svalbard 18:49:01 18:59:33 28.7 632
19 Svalbard 20:28:53 20:37:48 16.2 535
20 Alice Springs 21:04:50 21:12:26 12.5 456
21 Svalbard 22:09:40 22:16:17 9.8 397
22 Alice Springs 22:40:14 22:51:17 42.2 663
23 Sioux Falls 23:39:43 23:46:08 9.9 385
24 Svalbard 23:50:41 23:55:21 7.1 280
[4]:
# Plot one of the contacts
contact = data.iloc[5]
import plotly.graph_objects as go
from plotly.subplots import make_subplots

fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.1,
                        subplot_titles=('Range [km]', 'Elevation [deg]', 'Heading [deg]'))
fig.add_trace(go.Scatter(x=contact['timearr'], y=contact['range_km'], name='Range [km]'), row=1, col=1)
fig.add_trace(go.Scatter(x=contact['timearr'], y=contact['elevation_deg'], name='Elevation [deg]'), row=2, col=1)
fig.add_trace(go.Scatter(x=contact['timearr'], y=contact['heading_deg'], name='Heading [deg]'), row=3, col=1)
fig.update_layout(yaxis = {'title': 'Range (km)'},
                yaxis2={'title': 'Elevation [deg]'},
                yaxis3={'title': 'Heading [deg]'},
                title=f'Landsat 7 to {contact["Ground Station"]} on {contact["Start (UTC)"]}',
                width=800,
                height=600
                )