Skip to content

Propagation

Propagation loss models implementing the PropagationModel protocol.

Free-Space Path Loss

FreeSpacePropagation

Free-space path loss model.

Computes FSPL(dB) = 20 * log10(4 * pi * d * f / c) where d is the slant range and f is the carrier frequency.

Examples:

>>> prop = FreeSpacePropagation()
>>> prop.total_path_loss_db(12e9, 30.0, 37e6, PropagationConditions())
205.3  # approximate

total_path_loss_db

total_path_loss_db(f_hz, elev_deg, range_m, cond)

Compute free-space path loss.

Parameters:

Name Type Description Default
f_hz float

Carrier frequency in Hz.

required
elev_deg float

Elevation angle in degrees (unused for FSPL).

required
range_m float

Slant range in metres.

required
cond PropagationConditions

Environmental conditions (unused for FSPL).

required

Returns:

Type Description
float

Free-space path loss in dB (positive value).

Source code in src/opensatcom/propagation/fspl.py
def total_path_loss_db(
    self,
    f_hz: float,
    elev_deg: float,
    range_m: float,
    cond: PropagationConditions,
) -> float:
    """Compute free-space path loss.

    Parameters
    ----------
    f_hz : float
        Carrier frequency in Hz.
    elev_deg : float
        Elevation angle in degrees (unused for FSPL).
    range_m : float
        Slant range in metres.
    cond : PropagationConditions
        Environmental conditions (unused for FSPL).

    Returns
    -------
    float
        Free-space path loss in dB (positive value).
    """
    return 20.0 * math.log10(4.0 * math.pi * range_m * f_hz / SPEED_OF_LIGHT_MPS)

Composite Propagation

CompositePropagation

CompositePropagation(components)

Sum losses in dB across multiple propagation components.

Combines FSPL with atmospheric, rain, and scintillation losses. Each component's total_path_loss_db is called and the results are summed in dB domain.

Parameters:

Name Type Description Default
components Sequence of PropagationModel

Ordered list of propagation models to combine.

required

Examples:

>>> from opensatcom.propagation import FreeSpacePropagation
>>> comp = CompositePropagation([FreeSpacePropagation()])
Source code in src/opensatcom/propagation/composite.py
def __init__(self, components: Sequence[PropagationModel]) -> None:
    self.components = components

total_path_loss_db

total_path_loss_db(f_hz, elev_deg, range_m, cond)

Compute total composite path loss.

Parameters:

Name Type Description Default
f_hz float

Carrier frequency in Hz.

required
elev_deg float

Elevation angle in degrees.

required
range_m float

Slant range in metres.

required
cond PropagationConditions

Environmental conditions.

required

Returns:

Type Description
float

Sum of all component losses in dB.

Source code in src/opensatcom/propagation/composite.py
def total_path_loss_db(
    self,
    f_hz: float,
    elev_deg: float,
    range_m: float,
    cond: PropagationConditions,
) -> float:
    """Compute total composite path loss.

    Parameters
    ----------
    f_hz : float
        Carrier frequency in Hz.
    elev_deg : float
        Elevation angle in degrees.
    range_m : float
        Slant range in metres.
    cond : PropagationConditions
        Environmental conditions.

    Returns
    -------
    float
        Sum of all component losses in dB.
    """
    return sum(
        c.total_path_loss_db(f_hz, elev_deg, range_m, cond)
        for c in self.components
    )

per_component_losses_db

per_component_losses_db(f_hz, elev_deg, range_m, cond)

Compute per-component path losses.

Parameters:

Name Type Description Default
f_hz float

Carrier frequency in Hz.

required
elev_deg float

Elevation angle in degrees.

required
range_m float

Slant range in metres.

required
cond PropagationConditions

Environmental conditions.

required

Returns:

Type Description
dict of str to float

Mapping from component key name to its loss in dB. Keys use standard names: fspl_db, rain_attenuation_db, gaseous_absorption_db, scintillation_db.

Source code in src/opensatcom/propagation/composite.py
def per_component_losses_db(
    self,
    f_hz: float,
    elev_deg: float,
    range_m: float,
    cond: PropagationConditions,
) -> dict[str, float]:
    """Compute per-component path losses.

    Parameters
    ----------
    f_hz : float
        Carrier frequency in Hz.
    elev_deg : float
        Elevation angle in degrees.
    range_m : float
        Slant range in metres.
    cond : PropagationConditions
        Environmental conditions.

    Returns
    -------
    dict of str to float
        Mapping from component key name to its loss in dB.
        Keys use standard names: ``fspl_db``, ``rain_attenuation_db``,
        ``gaseous_absorption_db``, ``scintillation_db``.
    """
    result: dict[str, float] = {}
    for comp in self.components:
        class_name = type(comp).__name__
        key = _COMPONENT_KEY_MAP.get(class_name, f"{class_name}_db")
        result[key] = comp.total_path_loss_db(f_hz, elev_deg, range_m, cond)
    return result

Rain Attenuation (ITU-R P.618)

RainAttenuationP618

RainAttenuationP618(availability_target=0.99, rain_rate_mm_per_hr=None, climate_region=None)

ITU-R P.618 rain attenuation model.

Computes rain-induced path loss using specific attenuation from ITU-R P.838 coefficients and effective path length derived from elevation angle.

Parameters:

Name Type Description Default
availability_target float

Target link availability (e.g. 0.99 for 99%).

0.99
rain_rate_mm_per_hr float | None

Rain rate. If None, uses value from PropagationConditions.

None
climate_region str | None

ITU rain climate region (for future region-based rain rate lookup).

None
Source code in src/opensatcom/propagation/rain.py
def __init__(
    self,
    availability_target: float = 0.99,
    rain_rate_mm_per_hr: float | None = None,
    climate_region: str | None = None,
) -> None:
    self.availability_target = availability_target
    self._rain_rate = rain_rate_mm_per_hr
    self._climate_region = climate_region

total_path_loss_db

total_path_loss_db(f_hz, elev_deg, range_m, cond)

Compute rain attenuation in dB (additional loss beyond FSPL).

Parameters:

Name Type Description Default
f_hz float

Carrier frequency in Hz.

required
elev_deg float

Elevation angle in degrees.

required
range_m float

Slant range in metres (unused; path length derived from elevation).

required
cond PropagationConditions

Environmental conditions; rain_rate_mm_per_hr is used as fallback when not set at construction time.

required

Returns:

Type Description
float

Rain attenuation in dB (0.0 if rain rate is zero or frequency < 1 GHz).

Source code in src/opensatcom/propagation/rain.py
def total_path_loss_db(
    self,
    f_hz: float,
    elev_deg: float,
    range_m: float,
    cond: PropagationConditions,
) -> float:
    """Compute rain attenuation in dB (additional loss beyond FSPL).

    Parameters
    ----------
    f_hz : float
        Carrier frequency in Hz.
    elev_deg : float
        Elevation angle in degrees.
    range_m : float
        Slant range in metres (unused; path length derived from elevation).
    cond : PropagationConditions
        Environmental conditions; ``rain_rate_mm_per_hr`` is used as
        fallback when not set at construction time.

    Returns
    -------
    float
        Rain attenuation in dB (0.0 if rain rate is zero or frequency < 1 GHz).
    """
    rain_rate = self._rain_rate
    if rain_rate is None and cond.rain_rate_mm_per_hr is not None:
        rain_rate = cond.rain_rate_mm_per_hr
    if rain_rate is None or rain_rate <= 0.0:
        return 0.0

    f_ghz = f_hz / 1e9
    if f_ghz < 1.0:
        return 0.0

    k, alpha = _interpolate_p838(f_ghz)

    # Specific attenuation (dB/km)
    gamma_r = k * (rain_rate ** alpha)

    # Effective path length through rain (simplified P.618)
    # Rain height ~= 3.0 km for mid-latitudes (simplified)
    h_rain_km = 3.0
    h_station_km = 0.0  # Approximate ground level

    elev_rad = math.radians(max(elev_deg, 5.0))  # Clamp to avoid division issues
    sin_elev = math.sin(elev_rad)

    # Slant path length through rain
    l_s_km = (h_rain_km - h_station_km) / sin_elev

    # Horizontal reduction factor (simplified)
    l_g_km = l_s_km * math.cos(elev_rad)
    denom = (
        1.0
        + 0.78 * math.sqrt(l_g_km * gamma_r / f_ghz)
        - 0.38 * (1.0 - math.exp(-2.0 * l_g_km))
    )
    r_001 = 1.0 / denom

    # Effective path length
    l_eff_km = l_s_km * r_001

    # Rain attenuation exceeded for 0.01% of time
    a_001 = gamma_r * l_eff_km

    # Scale to desired availability using power-law approximation
    p_target = (1.0 - self.availability_target) * 100.0  # percentage exceedance
    if p_target <= 0.0:
        p_target = 0.01  # minimum

    if p_target >= 1.0:
        # For p >= 1%, attenuation is much less
        a_p = a_001 * 0.12 * (p_target ** 0.546)
    else:
        # Scaling from 0.01% to target percentage
        ratio = p_target / 0.01
        if ratio >= 1.0:
            beta = -0.655 + 0.033 * math.log(ratio) - 0.045 * math.log(a_001)
            beta = max(beta, -0.7)
            exp = -(
                0.655
                + 0.033 * math.log(ratio)
                - 0.045 * math.log(max(a_001, 0.01))
            )
            a_p = a_001 * (ratio ** exp)
        else:
            a_p = a_001

    return float(max(a_p, 0.0))

Gaseous Absorption (ITU-R P.676)

GaseousAbsorptionP676

GaseousAbsorptionP676(water_vapor_density_g_m3=7.5)

ITU-R P.676 gaseous absorption model.

Computes combined dry-air (O2) and water-vapor (H2O) absorption loss. Simplified model with frequency-dependent specific attenuation multiplied by slant path length through the atmosphere.

Parameters:

Name Type Description Default
water_vapor_density_g_m3 float

Surface water vapor density in g/m^3. Default 7.5 g/m^3 (standard mid-latitude).

7.5
Source code in src/opensatcom/propagation/gas.py
def __init__(self, water_vapor_density_g_m3: float = 7.5) -> None:
    self.rho = water_vapor_density_g_m3

total_path_loss_db

total_path_loss_db(f_hz, elev_deg, range_m, cond)

Compute gaseous absorption loss in dB.

Parameters:

Name Type Description Default
f_hz float

Carrier frequency in Hz.

required
elev_deg float

Elevation angle in degrees.

required
range_m float

Slant range in metres (unused; path derived from elevation).

required
cond PropagationConditions

Environmental conditions (unused; water vapor set at construction).

required

Returns:

Type Description
float

Combined dry-air and water-vapor absorption in dB (0.0 if frequency < 1 GHz).

Source code in src/opensatcom/propagation/gas.py
def total_path_loss_db(
    self,
    f_hz: float,
    elev_deg: float,
    range_m: float,
    cond: PropagationConditions,
) -> float:
    """Compute gaseous absorption loss in dB.

    Parameters
    ----------
    f_hz : float
        Carrier frequency in Hz.
    elev_deg : float
        Elevation angle in degrees.
    range_m : float
        Slant range in metres (unused; path derived from elevation).
    cond : PropagationConditions
        Environmental conditions (unused; water vapor set at construction).

    Returns
    -------
    float
        Combined dry-air and water-vapor absorption in dB
        (0.0 if frequency < 1 GHz).
    """
    f_ghz = f_hz / 1e9
    if f_ghz < 1.0:
        return 0.0

    gamma_dry = _specific_dry_attenuation(f_ghz)
    gamma_wet = _specific_wet_attenuation(f_ghz, self.rho)

    # Equivalent atmosphere height (simplified)
    # Dry: ~6 km effective height, Wet: ~2.1 km effective height
    h_dry_km = 6.0
    h_wet_km = 2.1

    elev_rad = math.radians(max(elev_deg, 5.0))
    sin_elev = math.sin(elev_rad)

    # Slant path attenuation
    a_dry = gamma_dry * h_dry_km / sin_elev
    a_wet = gamma_wet * h_wet_km / sin_elev

    return a_dry + a_wet

Scintillation

ScintillationLoss

ScintillationLoss(availability_target=0.99)

Tropospheric scintillation fade margin model.

Computes the scintillation fade margin based on frequency, elevation angle, and target availability. Uses a simplified model:

sigma = C_f * f_GHz^(7/12) * (1/sin(elev))^1.2
fade_margin = sigma * G(p)

where G(p) is the inverse Gaussian quantile for the availability target.

Parameters:

Name Type Description Default
availability_target float

Target link availability (e.g. 0.99 for 99%).

0.99
Source code in src/opensatcom/propagation/scintillation.py
def __init__(self, availability_target: float = 0.99) -> None:
    self.availability_target = availability_target

total_path_loss_db

total_path_loss_db(f_hz, elev_deg, range_m, cond)

Compute scintillation fade margin in dB.

Parameters:

Name Type Description Default
f_hz float

Carrier frequency in Hz.

required
elev_deg float

Elevation angle in degrees.

required
range_m float

Slant range in metres (unused; derived from elevation).

required
cond PropagationConditions

Environmental conditions; availability_target overrides the constructor value if set.

required

Returns:

Type Description
float

Scintillation fade margin in dB (0.0 if frequency < 1 GHz).

Source code in src/opensatcom/propagation/scintillation.py
def total_path_loss_db(
    self,
    f_hz: float,
    elev_deg: float,
    range_m: float,
    cond: PropagationConditions,
) -> float:
    """Compute scintillation fade margin in dB.

    Parameters
    ----------
    f_hz : float
        Carrier frequency in Hz.
    elev_deg : float
        Elevation angle in degrees.
    range_m : float
        Slant range in metres (unused; derived from elevation).
    cond : PropagationConditions
        Environmental conditions; ``availability_target`` overrides
        the constructor value if set.

    Returns
    -------
    float
        Scintillation fade margin in dB (0.0 if frequency < 1 GHz).
    """
    f_ghz = f_hz / 1e9
    if f_ghz < 1.0:
        return 0.0

    # Use availability from conditions if available, else constructor value
    avail = self.availability_target
    if cond.availability_target is not None:
        avail = cond.availability_target

    elev_rad = math.radians(max(elev_deg, 5.0))
    sin_elev = math.sin(elev_rad)

    # Scintillation standard deviation
    sigma = self._C_F * (f_ghz ** (7.0 / 12.0)) * ((1.0 / sin_elev) ** 1.2)

    # Fade margin for given availability
    g_p = _inverse_gaussian_quantile(avail)
    fade_db = sigma * g_p

    return float(max(fade_db, 0.0))