Svalbard Ground Station Access¶
Computes all contact windows between a sun-synchronous satellite and the Svalbard Satellite Station (SvalSat) over a 7-day period.
Scenario
550 km sun-synchronous orbit, LTDN 10:30 (descending node)
Ground station: SvalSat, 78.229°N / 15.407°E / 500 m altitude
Minimum elevation: 5°
Analysis window: 2025-03-01 → 2025-03-08 (7 days)
[1]:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime
from missiontools import Spacecraft, GroundStation
1. Spacecraft¶
A 550 km SSO with a 10:30 local time at descending node, propagated with J2.
[2]:
EPOCH = np.datetime64('2025-03-01T00:00:00', 'us')
sc = Spacecraft.sunsync(
altitude_km = 550.0,
node_solar_time = '10:30',
node_type = 'descending',
epoch = EPOCH,
)
period_s = 2 * np.pi * np.sqrt(sc.a**3 / sc.central_body_mu)
print(f"Semi-major axis : {sc.a / 1e3:.1f} km")
print(f"Altitude : {(sc.a - sc.central_body_radius) / 1e3:.1f} km")
print(f"Inclination : {np.degrees(sc.i):.3f}°")
print(f"RAAN at epoch : {np.degrees(sc.raan):.3f}°")
print(f"Orbital period : {period_s / 60:.2f} min")
print(f"Propagator : {sc.propagator_type}")
Semi-major axis : 6928.1 km
Altitude : 550.0 km
Inclination : 97.593°
RAAN at epoch : 139.643°
Orbital period : 95.65 min
Propagator : j2
2. Ground Station¶
SvalSat sits on the island of Spitsbergen at 78.229°N — so far north that it can see every polar-orbiting satellite at least twice per day.
[3]:
gs = GroundStation(lat=78.229, lon=15.407, alt=500.0)
print(f"Ground station : SvalSat")
print(f"Latitude : {gs.lat:.3f}°N")
print(f"Longitude : {gs.lon:.3f}°E")
print(f"Altitude : {gs.alt:.0f} m a.s.l.")
Ground station : SvalSat
Latitude : 78.229°N
Longitude : 15.407°E
Altitude : 500 m a.s.l.
3. Compute Access Windows¶
GroundStation.access() scans the analysis window at the requested maximum time step and returns a list of (AOS, LOS) pairs as datetime64[us] tuples. A 20 s step is fine for detecting passes whose shortest duration exceeds 5 min.
[4]:
T_START = np.datetime64('2025-03-01T00:00:00', 'us')
T_END = np.datetime64('2025-03-08T00:00:00', 'us')
passes = gs.access(
sc,
T_START, T_END,
el_min = 5.0, # degrees
max_step = np.timedelta64(20, 's'),
)
print(f"Total passes found: {len(passes)}")
Total passes found: 96
4. Access Report¶
[5]:
print(f" {'#':>3} {'AOS (UTC)':^26} {'LOS (UTC)':^26} {'Duration':>9}")
print(f" {'─'*3} {'─'*26} {'─'*26} {'─'*9}")
total_s = 0
for idx, (aos, los) in enumerate(passes, 1):
dur_s = int((los - aos) / np.timedelta64(1, 's'))
total_s += dur_s
print(f" {idx:>3} {str(aos):^26} {str(los):^26} "
f"{dur_s // 60:3d}m {dur_s % 60:02d}s")
window_s = int((T_END - T_START) / np.timedelta64(1, 's'))
print()
print(f" Total passes : {len(passes)}")
print(f" Total contact time : {total_s // 3600}h "
f"{(total_s % 3600) // 60}m {total_s % 60:02d}s")
print(f" Duty cycle : {total_s / window_s * 100:.2f}%")
print(f" Mean pass duration : {total_s / len(passes) / 60:.1f} min")
# AOS (UTC) LOS (UTC) Duration
─── ────────────────────────── ────────────────────────── ─────────
1 2025-03-01T01:10:53.750000 2025-03-01T01:18:22.500000 7m 28s
2 2025-03-01T02:46:01.250000 2025-03-01T02:55:08.125000 9m 06s
3 2025-03-01T04:21:03.750000 2025-03-01T04:30:53.750000 9m 50s
4 2025-03-01T05:55:56.250000 2025-03-01T06:05:52.500000 9m 56s
5 2025-03-01T07:30:36.250000 2025-03-01T07:40:21.250000 9m 45s
6 2025-03-01T09:05:02.500000 2025-03-01T09:14:42.500000 9m 40s
7 2025-03-01T10:39:23.750000 2025-03-01T10:49:11.250000 9m 47s
8 2025-03-01T12:13:56.875000 2025-03-01T12:23:53.750000 9m 56s
9 2025-03-01T13:49:03.125000 2025-03-01T13:58:48.750000 9m 45s
10 2025-03-01T15:25:00.000000 2025-03-01T15:33:52.500000 8m 52s
11 2025-03-01T17:01:57.500000 2025-03-01T17:09:00.625000 7m 03s
12 2025-03-01T18:39:59.375000 2025-03-01T18:44:07.500000 4m 08s
13 2025-03-01T23:30:31.250000 2025-03-01T23:35:25.000000 4m 53s
14 2025-03-02T01:05:40.000000 2025-03-02T01:13:12.500000 7m 32s
15 2025-03-02T02:40:47.500000 2025-03-02T02:49:55.625000 9m 08s
16 2025-03-02T04:15:49.375000 2025-03-02T04:25:40.625000 9m 51s
17 2025-03-02T05:50:41.875000 2025-03-02T06:00:37.500000 9m 55s
18 2025-03-02T07:25:21.250000 2025-03-02T07:35:06.250000 9m 45s
19 2025-03-02T08:59:47.500000 2025-03-02T09:09:26.875000 9m 39s
20 2025-03-02T10:34:08.125000 2025-03-02T10:43:56.250000 9m 48s
21 2025-03-02T12:08:42.500000 2025-03-02T12:18:39.375000 9m 56s
22 2025-03-02T13:43:50.000000 2025-03-02T13:53:34.375000 9m 44s
23 2025-03-02T15:19:48.125000 2025-03-02T15:28:38.125000 8m 50s
24 2025-03-02T16:56:48.125000 2025-03-02T17:03:46.875000 6m 58s
25 2025-03-02T18:34:51.250000 2025-03-02T18:38:53.750000 4m 02s
26 2025-03-02T23:25:17.500000 2025-03-02T23:30:16.250000 4m 58s
27 2025-03-03T01:00:25.625000 2025-03-03T01:08:01.875000 7m 36s
28 2025-03-03T02:35:33.125000 2025-03-03T02:44:43.750000 9m 10s
29 2025-03-03T04:10:35.625000 2025-03-03T04:20:26.875000 9m 51s
30 2025-03-03T05:45:27.500000 2025-03-03T05:55:23.125000 9m 55s
31 2025-03-03T07:20:06.250000 2025-03-03T07:29:51.250000 9m 45s
32 2025-03-03T08:54:32.500000 2025-03-03T09:04:11.875000 9m 39s
33 2025-03-03T10:28:53.125000 2025-03-03T10:38:41.250000 9m 48s
34 2025-03-03T12:03:28.125000 2025-03-03T12:13:25.000000 9m 56s
35 2025-03-03T13:38:36.875000 2025-03-03T13:48:20.000000 9m 43s
36 2025-03-03T15:14:36.875000 2025-03-03T15:23:24.375000 8m 47s
37 2025-03-03T16:51:38.125000 2025-03-03T16:58:32.500000 6m 54s
38 2025-03-03T18:29:43.125000 2025-03-03T18:33:39.375000 3m 56s
39 2025-03-03T21:45:31.250000 2025-03-03T21:45:55.625000 0m 24s
40 2025-03-03T23:20:03.750000 2025-03-03T23:25:07.500000 5m 03s
41 2025-03-04T00:55:11.875000 2025-03-04T01:02:51.250000 7m 39s
42 2025-03-04T02:30:19.375000 2025-03-04T02:39:31.875000 9m 12s
43 2025-03-04T04:05:21.250000 2025-03-04T04:15:13.125000 9m 51s
44 2025-03-04T05:40:13.125000 2025-03-04T05:50:08.125000 9m 55s
45 2025-03-04T07:14:51.875000 2025-03-04T07:24:36.250000 9m 44s
46 2025-03-04T08:49:16.875000 2025-03-04T08:58:56.875000 9m 40s
47 2025-03-04T10:23:38.125000 2025-03-04T10:33:26.875000 9m 48s
48 2025-03-04T11:58:13.125000 2025-03-04T12:08:10.000000 9m 56s
49 2025-03-04T13:33:23.125000 2025-03-04T13:43:06.250000 9m 43s
50 2025-03-04T15:09:25.000000 2025-03-04T15:18:10.625000 8m 45s
51 2025-03-04T16:46:28.125000 2025-03-04T16:53:18.750000 6m 50s
52 2025-03-04T18:24:35.000000 2025-03-04T18:28:25.625000 3m 50s
53 2025-03-04T21:40:09.375000 2025-03-04T21:40:56.250000 0m 46s
54 2025-03-04T23:14:49.375000 2025-03-04T23:19:58.750000 5m 09s
55 2025-03-05T00:49:58.125000 2025-03-05T00:57:40.625000 7m 42s
56 2025-03-05T02:25:05.625000 2025-03-05T02:34:19.375000 9m 13s
57 2025-03-05T04:00:06.875000 2025-03-05T04:09:59.375000 9m 52s
58 2025-03-05T05:34:58.750000 2025-03-05T05:44:53.750000 9m 55s
59 2025-03-05T07:09:36.875000 2025-03-05T07:19:20.625000 9m 43s
60 2025-03-05T08:44:01.875000 2025-03-05T08:53:41.875000 9m 40s
61 2025-03-05T10:18:23.125000 2025-03-05T10:28:11.875000 9m 48s
62 2025-03-05T11:52:58.750000 2025-03-05T12:02:55.625000 9m 56s
63 2025-03-05T13:28:10.000000 2025-03-05T13:37:51.875000 9m 41s
64 2025-03-05T15:04:13.750000 2025-03-05T15:12:56.250000 8m 42s
65 2025-03-05T16:41:18.125000 2025-03-05T16:48:05.000000 6m 46s
66 2025-03-05T18:19:26.875000 2025-03-05T18:23:11.250000 3m 44s
67 2025-03-05T21:34:50.625000 2025-03-05T21:35:53.125000 1m 02s
68 2025-03-05T23:09:35.625000 2025-03-05T23:14:50.000000 5m 14s
69 2025-03-06T00:44:44.375000 2025-03-06T00:52:30.000000 7m 45s
70 2025-03-06T02:19:51.250000 2025-03-06T02:29:06.875000 9m 15s
71 2025-03-06T03:54:52.500000 2025-03-06T04:04:45.625000 9m 53s
72 2025-03-06T05:29:43.750000 2025-03-06T05:39:38.750000 9m 55s
73 2025-03-06T07:04:21.875000 2025-03-06T07:14:05.625000 9m 43s
74 2025-03-06T08:38:46.875000 2025-03-06T08:48:26.875000 9m 40s
75 2025-03-06T10:13:08.125000 2025-03-06T10:22:56.875000 9m 48s
76 2025-03-06T11:47:44.375000 2025-03-06T11:57:41.875000 9m 57s
77 2025-03-06T13:22:56.875000 2025-03-06T13:32:38.125000 9m 41s
78 2025-03-06T14:59:01.875000 2025-03-06T15:07:42.500000 8m 40s
79 2025-03-06T16:36:08.125000 2025-03-06T16:42:51.250000 6m 43s
80 2025-03-06T18:14:18.750000 2025-03-06T18:17:56.875000 3m 38s
81 2025-03-06T21:29:33.125000 2025-03-06T21:30:48.750000 1m 15s
82 2025-03-06T23:04:21.875000 2025-03-06T23:09:41.250000 5m 19s
83 2025-03-07T00:39:30.625000 2025-03-07T00:47:19.375000 7m 48s
84 2025-03-07T02:14:37.500000 2025-03-07T02:23:55.000000 9m 17s
85 2025-03-07T03:49:38.750000 2025-03-07T03:59:31.875000 9m 53s
86 2025-03-07T05:24:29.375000 2025-03-07T05:34:23.750000 9m 54s
87 2025-03-07T06:59:06.875000 2025-03-07T07:08:50.625000 9m 43s
88 2025-03-07T08:33:31.250000 2025-03-07T08:43:11.875000 9m 40s
89 2025-03-07T10:07:53.125000 2025-03-07T10:17:42.500000 9m 49s
90 2025-03-07T11:42:30.625000 2025-03-07T11:52:27.500000 9m 56s
91 2025-03-07T13:17:43.750000 2025-03-07T13:27:23.750000 9m 40s
92 2025-03-07T14:53:50.625000 2025-03-07T15:02:28.750000 8m 38s
93 2025-03-07T16:30:58.750000 2025-03-07T16:37:37.500000 6m 38s
94 2025-03-07T18:09:10.625000 2025-03-07T18:12:43.125000 3m 32s
95 2025-03-07T21:24:16.875000 2025-03-07T21:25:43.750000 1m 26s
96 2025-03-07T22:59:08.125000 2025-03-07T23:04:32.500000 5m 24s
Total passes : 96
Total contact time : 12h 54m 58s
Duty cycle : 7.69%
Mean pass duration : 8.1 min
5. Pass Timeline¶
Each bar represents one contact window. The bar length is proportional to contact duration.
[6]:
# Convert datetime64 to Python datetime for matplotlib
def to_dt(t64):
return t64.astype('datetime64[ms]').astype(datetime)
fig, ax = plt.subplots(figsize=(12, 4))
for idx, (aos, los) in enumerate(passes):
dur_s = int((los - aos) / np.timedelta64(1, 's'))
ax.barh(
0,
left = to_dt(aos),
width = to_dt(los) - to_dt(aos),
height = 0.5,
color = 'tab:blue',
alpha = 0.7,
)
# Annotate short passes with their duration (min:ss)
mid = to_dt(aos + (los - aos) // 2)
ax.text(mid, 0, f"{dur_s // 60}m{dur_s % 60:02d}s",
ha='center', va='center', fontsize=7, color='white', fontweight='bold')
ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %d'))
ax.xaxis.set_major_locator(mdates.DayLocator())
ax.set_yticks([])
ax.set_xlim(to_dt(T_START), to_dt(T_END))
ax.set_xlabel('Date (UTC)')
ax.set_title(
f'SvalSat contact windows — 550 km SSO LTDN 10:30, el ≥ 5°\n'
f'{len(passes)} passes, {total_s / 3600:.1f} h total contact time'
)
ax.grid(True, axis='x', alpha=0.3)
plt.tight_layout()
plt.show()
6. Passes per Day¶
[7]:
from collections import defaultdict
passes_per_day = defaultdict(list)
for aos, los in passes:
day = str(aos)[:10] # 'YYYY-MM-DD'
dur_s = int((los - aos) / np.timedelta64(1, 's'))
passes_per_day[day].append(dur_s)
print(f" {'Date':>12} {'Passes':>7} {'Contact':>12}")
print(f" {'─'*12} {'─'*7} {'─'*12}")
for day in sorted(passes_per_day):
n = len(passes_per_day[day])
t = sum(passes_per_day[day])
print(f" {day:>12} {n:>7} {t // 60:3d}m {t % 60:02d}s")
Date Passes Contact
──────────── ─────── ────────────
2025-03-01 13 110m 09s
2025-03-02 13 110m 06s
2025-03-03 14 110m 27s
2025-03-04 14 110m 48s
2025-03-05 14 110m 58s
2025-03-06 14 111m 12s
2025-03-07 14 111m 18s