Scripting Satellite Link Budgets with ITU-Rpy (Python Examples)
STK-free link budgets: sweep frequency, rain availability, and elevation in pure Python with ITU-Rpy, the reference implementation of P.618/P.676/P.840.
Why script your link budgets
The rftools Satellite Link Budget Analyzer is built for interactive scenario design. When you need to sweep a parameter across hundreds of points — for trade studies, frequency-plan optimization, or CI/CD integration into your mission-design repo — you want Python, not a browser.
ITU-Rpy is the open-source reference implementation of the ITU-R Recommendations for Earth-space propagation. The analyzer's propagation code is a port of it and agrees with it within 0.001 % at twelve test sites, once both are set to the same Recommendation versions (see the end of this post). This post shows three scripted use cases you can lift straight into a Jupyter notebook.Install
pip install itur numpy matplotlib
ITU-Rpy pulls its own data tables (rain rate maps, refractive index climatologies) from the ITU repository. First import may take ~5 seconds.
Use case 1: Frequency sweep for a Ka-band downlink
You're designing a GEO broadcast downlink and want to know how specific rain attenuation varies from 20 to 40 GHz at 99.99% availability.
import itur
import itur.models as m
import numpy as np
import matplotlib.pyplot as plt
# Ground station: Atlanta, GA
lat, lon = 33.75, -84.39
elevation_deg = 45.0
availability_pct = 0.01 # 0.01% outage → 99.99% availability
freqs = np.linspace(20, 40, 21) # GHz
attenuations = []
for f in freqs:
a = itur.atmospheric_attenuation_slant_path(
lat=lat, lon=lon,
f=f * itur.u.GHz,
el=elevation_deg,
p=availability_pct,
D=1.2 * itur.u.m, # antenna diameter (used for the scintillation term)
hs=0.3 * itur.u.km,
)
attenuations.append(float(a.value))
plt.plot(freqs, attenuations)
plt.xlabel('Frequency (GHz)')
plt.ylabel('Total atmospheric attenuation (dB)')
plt.title(f'Ka-band slant path, {availability_pct}% outage, Atlanta 45° elevation')
plt.grid(True)
plt.show()
Atlanta is wet: the P.837-7 map gives = 51.9 mm/h. At 99.99 % availability the script returns 25.0 dB of total attenuation at 20 GHz — 22.9 dB of it rain — 47.8 dB at 30 GHz and 70.6 dB at 40 GHz. No practical fade margin covers that at 40 GHz, which is why Ka-band and Q/V-band systems rely on uplink power control, adaptive coding and site diversity rather than static margin, and quote Q/V-band availabilities well below 99.99 %.
Use case 2: Availability sweep for a rain-fade trade study
You need to decide between 99.9 % (526 minutes of outage a year), 99.99 % (53 minutes) and 99.999 % (5 minutes) for a VSAT terminal. How much extra fade margin does each tier cost?
availabilities = [1.0, 0.1, 0.01, 0.001] # 99% through 99.999%
freq = 14.5 # Ku-band uplink
lat, lon = 42.0, -3.0 # northern Spain (P.837-7 R0.01 = 26.7 mm/h)
print(f'{"Availability":14s} {"Outage/yr":12s} {"Total Atten":12s}')
print('-' * 40)
for p in availabilities:
a = itur.atmospheric_attenuation_slant_path(
lat=lat, lon=lon,
f=freq * itur.u.GHz,
el=30.0,
p=p,
D=1.0 * itur.u.m, # required: antenna diameter for the scintillation term
)
outage_min = p / 100 * 365.25 * 24 * 60
pct = 100 - p
print(f'{pct:9.3f} % {outage_min:9.1f} min {float(a.value):8.2f} dB')
Typical output:
Availability Outage/yr Total Atten
----------------------------------------
99.000 % 5259.6 min 0.89 dB
99.900 % 526.0 min 2.22 dB
99.990 % 52.6 min 5.96 dB
99.999 % 5.3 min 12.75 dB
Going from 99.99 % to 99.999 % costs another 6.8 dB at 14.5 GHz — more than the whole step from 99 % to 99.99 %. For most VSAT services that last nine is not worth the hardware, and the table makes the conversation with stakeholders easy. (With ITU-Rpy's defaults these totals include a small scintillation term; see below.)
Use case 3: Elevation mask — where does the link close at low elevation?
For a LEO constellation with a fixed ground terminal, low-elevation passes have much higher atmospheric attenuation. Plot total attenuation vs elevation:
elevations = np.arange(5, 85, 5)
attens = []
for el in elevations:
a = itur.atmospheric_attenuation_slant_path(
lat=51.5, lon=-0.1, # London
f=20.0 * itur.u.GHz,
el=el,
p=0.1,
D=1.0 * itur.u.m,
)
attens.append(float(a.value))
plt.plot(elevations, attens)
plt.xlabel('Elevation angle (deg)')
plt.ylabel('Total attenuation (dB) at 20 GHz, 99.9%')
plt.title('Atmospheric + rain attenuation vs elevation — London')
plt.grid(True)
plt.show()
At 99.9 %, London sees 4.9 dB at 45°, 8.3 dB at 20°, 14.2 dB at 10° and 25.0 dB at 5° — five times the 45° figure. That is why LEO Ka-band terminals use a minimum elevation mask of 15–20° and deprioritize low-elevation contacts.
Feeding the output back into the rftools Analyzer
You don't need to copy attenuation numbers into the Satellite Link Budget Analyzer: give it the site's latitude and longitude and it reads the same ITU-R maps and runs the same methods, then adds the availability curve and the Monte Carlo. To reproduce its numbers exactly in ITU-Rpy, match the Recommendation versions and leave out scintillation, which the analyzer does not model:
from itur.models import itu618, itu676, itu836, itu837, itu838, itu839, itu840, itu1510, itu1511
itu618.change_version(13); itu676.change_version(12); itu836.change_version(6)
itu837.change_version(7); itu838.change_version(3); itu839.change_version(4)
itu840.change_version(8); itu1510.change_version(1); itu1511.change_version(1)
ag, ac, ar, _, _ = itur.atmospheric_attenuation_slant_path(
lat, lon, f * itur.u.GHz, el, p, 1.0 * itur.u.m,
tau=0, # horizontal; 90 vertical, 45 circular
return_contributions=True, include_scintillation=False,
)
ITU-Rpy 0.4.0 defaults to P.840-7 and to circular polarization (tau=45); the analyzer uses P.840-8 and the polarization you choose.
If you already have an attenuation figure from elsewhere and want to test a budget with it, the RF Link Budget calculator takes rain fade and atmospheric loss as plain inputs, and our POST /api/py/v1/calculate endpoint accepts it with all inputs — handy for CI pipelines that check link-budget compliance on every commit.
Common gotchas
- Units matter.
f=20.0without* itur.u.GHzsilently assumes Hz. ITU-Rpy validates dimensionally. - Availability is outage %, not uptime %.
p=0.01means 99.99% availability (0.01% of time the attenuation exceeds that value). - Rain rate comes from the P.837 map at lat/lon. P.837-7 has no rain zones. To test a wet tropical site, pass one, e.g.
lat=1.35, lon=103.8(Singapore, ≈ 100 mm/h) — or override the map withR001=. - The antenna diameter
Dfeeds the scintillation term, which falls as the aperture averages over more of the turbulent layer. It does not affect cloud attenuation. - Scintillation is included by default.
atmospheric_attenuation_slant_pathadds it unless you passinclude_scintillation=False. Usereturn_contributions=Trueto see gas, cloud, rain and scintillation separately.
Replacing STK Cloud's Comm module
STK Cloud's Comm/Radar suite wrapped these ITU-R calculations in a UI. For scripting, ITU-Rpy is the reference implementation they would have called. Migration:
| STK Cloud Comm | ITU-Rpy equivalent |
|---|---|
| Rain attenuation (slant path) | itur.models.itu618.rain_attenuation() |
| Specific rain attenuation | itur.models.itu838.rain_specific_attenuation() |
| Gaseous absorption | itur.models.itu676.gaseous_attenuation_slant_path() |
| Cloud attenuation | itur.models.itu840.cloud_attenuation() |
| Scintillation | itur.models.itu618.scintillation_attenuation() |
| Total attenuation | itur.atmospheric_attenuation_slant_path() |
For anything short of a full-blown constellation simulation, this is what you want. See also Migrating from STK Cloud for the complete tool-replacement map.