Skip to content

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.

Satellite Communications5 min readUses the RF Link Budget Calculator

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.

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 R0.01R_{0.01} = 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.)

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

  1. Units matter. f=20.0 without * itur.u.GHz silently assumes Hz. ITU-Rpy validates dimensionally.
  2. Availability is outage %, not uptime %. p=0.01 means 99.99% availability (0.01% of time the attenuation exceeds that value).
  3. 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, R0.01R_{0.01} ≈ 100 mm/h) — or override the map with R001=.
  4. The antenna diameter D feeds the scintillation term, which falls as the aperture averages over more of the turbulent layer. It does not affect cloud attenuation.
  5. Scintillation is included by default. atmospheric_attenuation_slant_path adds it unless you pass include_scintillation=False. Use return_contributions=True to 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 CommITU-Rpy equivalent
Rain attenuation (slant path)itur.models.itu618.rain_attenuation()
Specific rain attenuationitur.models.itu838.rain_specific_attenuation()
Gaseous absorptionitur.models.itu676.gaseous_attenuation_slant_path()
Cloud attenuationitur.models.itu840.cloud_attenuation()
Scintillationitur.models.itu618.scintillation_attenuation()
Total attenuationitur.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.

Further reading

Updated 2026-09-19: the attenuation figures above were recomputed with ITU-Rpy 0.4.0. Earlier values understated Ka-band rain in Atlanta by an order of magnitude, and the notes on antenna diameter, scintillation and feeding numbers into the analyzer were wrong.

Frequently asked questions

Yes. ITU-Rpy is the open-source reference implementation of ITU-R Recommendations P.618, P.676, P.840, P.838, P.837, P.839, P.836 and P.530, validated against the ITU's own test vectors. The rftools Satellite Link Budget Analyzer is a port of it and agrees with it within 0.001 % when both use the same Recommendation versions.
Rain attenuation (ITU-R P.618) is only the rain component. Total atmospheric attenuation combines rain, gaseous absorption (P.676), cloud liquid water (P.840) and, in ITU-Rpy's default call, scintillation (P.618), as P.618 section 2.5 specifies. Gas and cloud dominate at low availability targets such as 99 %, where rain is small; rain dominates increasingly as the target rises toward 99.99 % and beyond.
outage_minutes_per_year = (outage_pct / 100) * 525,960 minutes. So 99.99% availability = 0.01% outage = 52.6 min/year. 99.999% = 0.001% outage = 5.3 min/year. The ITU-R p parameter is always the outage percentage, not the availability percentage — common source of mistakes.
The ITU-R P.676 gaseous absorption model is valid from 1 GHz to 1000 GHz. P.618 rain attenuation is specified for 4-55 GHz. For W-band (75-110 GHz) and above, use P.840 cloud attenuation + P.676 gaseous; rain attenuation extrapolates reasonably for scientific missions but lacks regulatory validation above 55 GHz.
At a fixed availability, attenuation grows quickly as elevation falls — faster than 1/sin(elevation) at low angles, because the path spends longer in rain and gas. In London at 20 GHz, total attenuation exceeded 0.01 % of the year is 11.9 dB at 45° and 28.7 dB at 10°. This is why Ka-band and higher terminals use a minimum elevation mask, typically 15–20°.
Back to the blog