Canada Coverage Analysis — 4-Satellite Constellation

End-to-end demonstration of missiontools coverage and plotting features with a constellation.

Scenario

  • 4 satellites in a sun-synchronous orbit, 550 km, 10:30 LTAN (ascending)

  • Satellites evenly spaced in the orbital plane (90° separation in mean anomaly)

  • Nadir-pointed pushbroom sensor per satellite, 20° half-angle FOV

  • Min ground elevation: 20°

  • Illumination constraint: solar zenith angle < 70° (daytime)

  • Area of interest: Canada (20 000 km² point density)

  • Simulation window: 90 days

[1]:
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs

from missiontools import Spacecraft, ConicSensor, AoI, Coverage
from missiontools.plotting import plot_ground_track, plot_coverage_map

1. Build the constellation (4 evenly-spaced satellites)

[2]:
EPOCH = np.datetime64("2025-05-01T00:00:00", "us")

# Get sun-synchronous orbit parameters
sc0 = Spacecraft.sunsync(
    altitude_km=550.0,
    node_solar_time="10:30",
    epoch=EPOCH,
)

# Create 4 satellites evenly spaced in the orbital plane
NUM_SATELLITES = 4
spacecraft_list = []
sensor_list = []

for i in range(NUM_SATELLITES):
    # Create spacecraft with evenly-spaced mean anomaly
    sc = Spacecraft(
        a=sc0.a,
        e=sc0.e,
        i=sc0.i,
        raan=sc0.raan,
        arg_p=sc0.arg_p,
        ma=(2 * np.pi * i) / NUM_SATELLITES,  # 0°, 90°, 180°, 270°
        epoch=sc0.epoch,
        propagator_type="j2",
    )

    # Attach nadir-pointed sensor to each satellite
    sensor = ConicSensor(20.0, body_vector=[0, 0, 1])
    sc.add_sensor(sensor)

    spacecraft_list.append(sc)
    sensor_list.append(sensor)

print(f"Semi-major axis : {sc0.a/1e3:.1f} km")
print(f"Inclination     : {np.degrees(sc0.i):.2f} deg")
print(f"RAAN            : {np.degrees(sc0.raan):.2f} deg")
print(f"Propagator      : {sc0.propagator_type}")
print(f"Constellation   : {NUM_SATELLITES} satellites @ 0°, 90°, 180°, 270° mean anomaly")
Semi-major axis : 6928.1 km
Inclination     : 97.59 deg
RAAN            : 15.96 deg
Propagator      : j2
Constellation   : 4 satellites @ 0°, 90°, 180°, 270° mean anomaly

2. Define the Area of Interest

[3]:
# 20 000 km² per sample point  (~141 km spacing)
aoi = AoI.from_geography("Canada", point_density=20_000)
print(f"AoI sample points: {len(aoi)}")
AoI sample points: 644

3. Set up Coverage analysis

[4]:
cov = Coverage(
    aoi,
    sensor_list,
    el_min_deg=20.0,
    sza_max_deg=70.0,
)

4. Run the 90-day simulation

[5]:
T_START = EPOCH
T_END   = EPOCH + np.timedelta64(90 * 24 * 3600, "s")

print("Computing coverage fraction ...")
frac = cov.coverage_fraction(T_START, T_END, max_step=np.timedelta64(10, "s"))
print(f"  Final cumulative coverage : {frac["final_cumulative"]:.1%}")
print(f"  Mean instantaneous        : {frac["mean_fraction"]:.1%}")

print("Computing revisit times ...")
rev = cov.revisit_time(T_START, T_END, max_step=np.timedelta64(10, "s"))
Computing coverage fraction ...
  Final cumulative coverage : 100.0%
  Mean instantaneous        : 0.1%
Computing revisit times ...

5. Ground tracks — first 24 hours

[6]:
fig = plt.figure(figsize=(14, 6))
ax  = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())

colors = ["tab:orange", "tab:blue", "tab:green", "tab:red"]

for i, sc in enumerate(spacecraft_list):
    plot_ground_track(
        sc,
        T_START,
        T_START + np.timedelta64(24 * 3600, "s"),
        ax=ax,
        color=colors[i],
        label=f"Sat {i+1}",
        linewidth=0.8,
    )

ax.set_title("Ground track — first 24 hours (4 satellites)")
ax.legend(loc="lower left")
plt.tight_layout()
plt.show()
../_images/examples_canada_coverage_constellation_11_0.png

6. Coverage maps over Canada

We use a Lambert Conformal Conic projection with the same geometric parameters as EPSG:3347 (Statistics Canada Lambert). We define it manually because Cartopy’s carries restrictive area-of-use bounds from the EPSG registry that clip southern Canada out of the renderable area.

[7]:
# Statistics Canada Lambert — same geometry as EPSG:3347 but without
# the restrictive area-of-use bounds that clip southern Canada.
canada_proj = ccrs.LambertConformal(
    central_longitude=-91.867,
    central_latitude=63.391,
    standard_parallels=(49, 77),
)

# --- Mean revisit time (hours) -------------------------------------------
mean_rev_hrs = rev["mean_revisit"] / 3600.0  # seconds → hours

fig, ax = plt.subplots(
    figsize=(12, 7),
    subplot_kw={"projection": canada_proj},
)
plot_coverage_map(
    aoi,
    mean_rev_hrs,
    ax=ax,
    auto_window=True,
    cmap="plasma_r",
    colorbar_label="Mean revisit time (hours)",
title="90-day mean revisit time — Canada\n4 satellites, 550 km SSO 10:30 LTAN, 20° half-angle, el ≥ 20°, SZA < 70°",
)
plt.tight_layout()
plt.show()
../_images/examples_canada_coverage_constellation_13_0.png
[8]:
# --- Maximum revisit time (hours) -----------------------------------------
max_rev_hrs = rev["max_revisit"] / 3600.0  # seconds → hours

fig, ax = plt.subplots(
    figsize=(12, 7),
    subplot_kw={"projection": canada_proj},
)
plot_coverage_map(
    aoi,
    max_rev_hrs,
    ax=ax,
    auto_window=True,
    cmap="plasma_r",
    colorbar_label="Max revisit time (hours)",
title="90-day maximum revisit time — Canada\n4 satellites, 550 km SSO 10:30 LTAN, 20° half-angle, el ≥ 20°, SZA < 70°",
)
plt.tight_layout()
plt.show()
../_images/examples_canada_coverage_constellation_14_0.png

7. Summary statistics

[9]:
print("=" * 50)
print("90-day coverage summary — Canada (4-satellite constellation)")
print("=" * 50)
print(f"Cumulative coverage fraction : {frac["final_cumulative"]:.1%}")
print(f"Mean instantaneous coverage  : {frac["mean_fraction"]:.1%}")
print()
global_mean_hrs = rev["global_mean"] / 3600.0
global_max_hrs  = rev["global_max"]  / 3600.0
print(f"Global mean revisit time       : {global_mean_hrs:.1f} h")
print(f"Global max revisit time        : {global_max_hrs:.1f} h")

# Compare with single satellite results from canada_coverage.ipynb
single_mean = 59.8  # hours from single-satellite example
single_max = 893.9  # hours from single-satellite example

print()
print("--- Comparison with single satellite ---")
print(f"Mean revisit time improvement  : {single_mean/global_mean_hrs:.1f}x faster")
print(f"Max revisit time improvement   : {single_max/global_max_hrs:.1f}x faster")
==================================================
90-day coverage summary — Canada (4-satellite constellation)
==================================================
Cumulative coverage fraction : 100.0%
Mean instantaneous coverage  : 0.1%

Global mean revisit time       : 17.9 h
Global max revisit time        : 178.1 h

--- Comparison with single satellite ---
Mean revisit time improvement  : 3.3x faster
Max revisit time improvement   : 5.0x faster
[ ]: