Skip to content

Modem

DVB-S2 modem models — ModCod tables, performance curves, ACM policies, and throughput computation.

Modem Model

ModemModel

ModemModel(modcods, curves, target_bler, acm_policy)

Modem that maps (Eb/N0, bandwidth, time) to throughput.

Composes a ModCod table, performance curves, and an ACM policy to select the best modulation/coding and compute achievable throughput.

Parameters:

Name Type Description Default
modcods list of ModCod

Available modulation and coding schemes.

required
curves dict of str to PerformanceCurve

Performance curves keyed by ModCod name.

required
target_bler float

Target block error rate threshold.

required
acm_policy ACMPolicy

Adaptive coding and modulation selection policy.

required
Source code in src/opensatcom/modem/modem.py
def __init__(
    self,
    modcods: list[ModCod],
    curves: dict[str, PerformanceCurve],
    target_bler: float,
    acm_policy: ACMPolicy,
) -> None:
    self.modcods = modcods
    self.curves = curves
    self.target_bler = target_bler
    self.acm_policy = acm_policy

throughput_mbps

throughput_mbps(ebn0_db, bandwidth_hz, t_s)

Compute throughput for given channel conditions.

Parameters:

Name Type Description Default
ebn0_db float

Current Eb/N0 in dB.

required
bandwidth_hz float

Allocated bandwidth in Hz.

required
t_s float

Current simulation time in seconds.

required

Returns:

Type Description
dict

Result dictionary with keys:

  • "throughput_mbps" — achievable throughput in Mbps
  • "selected_modcod" — name of the selected ModCod
  • "spectral_eff_bps_per_hz" — net spectral efficiency
  • "bler_est" — estimated block error rate
Source code in src/opensatcom/modem/modem.py
def throughput_mbps(
    self, ebn0_db: float, bandwidth_hz: float, t_s: float
) -> dict[str, float | str]:
    """Compute throughput for given channel conditions.

    Parameters
    ----------
    ebn0_db : float
        Current Eb/N0 in dB.
    bandwidth_hz : float
        Allocated bandwidth in Hz.
    t_s : float
        Current simulation time in seconds.

    Returns
    -------
    dict
        Result dictionary with keys:

        - ``"throughput_mbps"`` — achievable throughput in Mbps
        - ``"selected_modcod"`` — name of the selected ModCod
        - ``"spectral_eff_bps_per_hz"`` — net spectral efficiency
        - ``"bler_est"`` — estimated block error rate
    """
    selected = self.acm_policy.select_modcod(ebn0_db, t_s)
    spec_eff = selected.net_spectral_eff_bps_per_hz()
    bler_est = self.curves[selected.name].bler(ebn0_db)
    throughput_bps = spec_eff * bandwidth_hz * (1.0 - bler_est)
    throughput_mbps = throughput_bps / 1e6

    return {
        "throughput_mbps": throughput_mbps,
        "selected_modcod": selected.name,
        "spectral_eff_bps_per_hz": spec_eff,
        "bler_est": bler_est,
    }

DVB-S2 Built-in Tables

dvbs2

DVB-S2 built-in ModCod table and performance curves.

get_dvbs2_modcod_table

get_dvbs2_modcod_table()

Return the full DVB-S2 ModCod table.

Returns:

Type Description
list of ModCod

28 standard DVB-S2 ModCods (QPSK through 32APSK).

Source code in src/opensatcom/modem/dvbs2.py
def get_dvbs2_modcod_table() -> list[ModCod]:
    """Return the full DVB-S2 ModCod table.

    Returns
    -------
    list of ModCod
        28 standard DVB-S2 ModCods (QPSK through 32APSK).
    """
    return list(DVB_S2_MODCODS)

get_dvbs2_performance_curves

get_dvbs2_performance_curves()

Return analytic performance curves for all DVB-S2 ModCods.

Returns:

Type Description
dict of str to PerformanceCurve

Analytic BER curves keyed by ModCod name.

Source code in src/opensatcom/modem/dvbs2.py
def get_dvbs2_performance_curves() -> dict[str, PerformanceCurve]:
    """Return analytic performance curves for all DVB-S2 ModCods.

    Returns
    -------
    dict of str to PerformanceCurve
        Analytic BER curves keyed by ModCod name.
    """
    curves: dict[str, PerformanceCurve] = {}
    for name, bps, cr, req_ebn0 in _DVB_S2_TABLE:
        curves[name] = AnalyticBERCurve(
            bits_per_symbol=bps,
            code_rate=cr,
            required_ebn0_ref_db=req_ebn0,
        )
    return curves

Performance Curves

TablePerformanceCurve

TablePerformanceCurve(points)

Table-based performance curve using interpolation.

Accepts a list of (ebn0_db, bler) points sorted by ebn0_db ascending. Uses np.interp for lookup — no scipy dependency.

Parameters:

Name Type Description Default
points list[tuple[float, float]]

List of (ebn0_db, bler) pairs defining the performance curve. Points are sorted internally by ebn0_db ascending.

required
Source code in src/opensatcom/modem/curves.py
def __init__(self, points: list[tuple[float, float]]) -> None:
    sorted_pts = sorted(points, key=lambda p: p[0])
    self._ebn0_db = np.array([p[0] for p in sorted_pts])
    self._bler = np.array([p[1] for p in sorted_pts])

bler

bler(ebn0_db)

Interpolate BLER at given Eb/N0.

Parameters:

Name Type Description Default
ebn0_db float

Energy per bit to noise spectral density ratio in dB.

required

Returns:

Type Description
float

Interpolated block error rate at the given Eb/N0.

Source code in src/opensatcom/modem/curves.py
def bler(self, ebn0_db: float) -> float:
    """Interpolate BLER at given Eb/N0.

    Parameters
    ----------
    ebn0_db : float
        Energy per bit to noise spectral density ratio in dB.

    Returns
    -------
    float
        Interpolated block error rate at the given Eb/N0.
    """
    return float(np.interp(ebn0_db, self._ebn0_db, self._bler))

required_ebn0_db

required_ebn0_db(target_bler)

Find Eb/N0 required to achieve target BLER.

Interpolates in reversed BLER→Eb/N0 direction (BLER decreases with Eb/N0).

Parameters:

Name Type Description Default
target_bler float

Desired block error rate threshold.

required

Returns:

Type Description
float

Required Eb/N0 in dB to achieve the target BLER.

Source code in src/opensatcom/modem/curves.py
def required_ebn0_db(self, target_bler: float) -> float:
    """Find Eb/N0 required to achieve target BLER.

    Interpolates in reversed BLER→Eb/N0 direction (BLER decreases with Eb/N0).

    Parameters
    ----------
    target_bler : float
        Desired block error rate threshold.

    Returns
    -------
    float
        Required Eb/N0 in dB to achieve the target BLER.
    """
    # BLER is monotonically decreasing with Eb/N0, so reverse for interp
    return float(
        np.interp(target_bler, self._bler[::-1], self._ebn0_db[::-1])
    )

AnalyticBERCurve

AnalyticBERCurve(bits_per_symbol, code_rate, required_ebn0_ref_db)

Analytic BER/BLER curve for coded modulation.

Uses closed-form approximations calibrated to DVB-S2 reference thresholds. For QPSK: BER ~ erfc(sqrt(Eb/N0 * code_rate)) For higher-order: M-PSK/M-QAM approximations with coding gain offset.

Parameters:

Name Type Description Default
bits_per_symbol float

Bits per modulation symbol (2 = QPSK, 3 = 8PSK, 4 = 16APSK, 5 = 32APSK).

required
code_rate float

FEC code rate (e.g., 0.5, 0.75).

required
required_ebn0_ref_db float

Reference required Eb/N0 in dB at BLER = 1e-5 from the DVB-S2 standard. This anchors the waterfall curve.

required
Source code in src/opensatcom/modem/analytic_curves.py
def __init__(
    self,
    bits_per_symbol: float,
    code_rate: float,
    required_ebn0_ref_db: float,
) -> None:
    self.bits_per_symbol = bits_per_symbol
    self.code_rate = code_rate
    self.required_ebn0_ref_db = required_ebn0_ref_db
    self._M = int(2 ** bits_per_symbol)

bler

bler(ebn0_db)

Estimate BLER at given Eb/N0 (dB).

Uses a waterfall-shaped curve centered on the reference threshold.

Parameters:

Name Type Description Default
ebn0_db float

Energy per bit to noise spectral density ratio in dB.

required

Returns:

Type Description
float

Estimated block error rate, clamped to [1e-10, 1.0].

Source code in src/opensatcom/modem/analytic_curves.py
def bler(self, ebn0_db: float) -> float:
    """Estimate BLER at given Eb/N0 (dB).

    Uses a waterfall-shaped curve centered on the reference threshold.

    Parameters
    ----------
    ebn0_db : float
        Energy per bit to noise spectral density ratio in dB.

    Returns
    -------
    float
        Estimated block error rate, clamped to [1e-10, 1.0].
    """
    # Distance from threshold in dB
    delta = ebn0_db - self.required_ebn0_ref_db

    # Waterfall curve: steep transition around threshold
    # BLER ~ 0.5 * erfc(k * delta) where k controls steepness
    k = 1.5  # Steepness factor typical for DVB-S2 LDPC codes
    bler_val = 0.5 * _erfc(k * delta)
    return max(min(bler_val, 1.0), 1e-10)

required_ebn0_db

required_ebn0_db(target_bler)

Find Eb/N0 required to achieve target BLER.

Inverts the waterfall curve analytically.

Parameters:

Name Type Description Default
target_bler float

Desired block error rate threshold.

required

Returns:

Type Description
float

Required Eb/N0 in dB to achieve the target BLER.

Source code in src/opensatcom/modem/analytic_curves.py
def required_ebn0_db(self, target_bler: float) -> float:
    """Find Eb/N0 required to achieve target BLER.

    Inverts the waterfall curve analytically.

    Parameters
    ----------
    target_bler : float
        Desired block error rate threshold.

    Returns
    -------
    float
        Required Eb/N0 in dB to achieve the target BLER.
    """
    if target_bler >= 0.5:
        return self.required_ebn0_ref_db - 5.0
    if target_bler <= 1e-10:
        return self.required_ebn0_ref_db + 5.0

    # Invert: target = 0.5 * erfc(k * delta)
    # erfc(k*delta) = 2*target
    # k*delta = erfc_inv(2*target)
    # Use Newton iteration on erfc to find x where erfc(x) = 2*target
    k = 1.5
    val = 2.0 * target_bler

    # Approximate inverse erfc using bisection
    lo, hi = -5.0, 10.0
    for _ in range(60):
        mid = (lo + hi) / 2.0
        if _erfc(mid) > val:
            lo = mid
        else:
            hi = mid
    x = (lo + hi) / 2.0
    delta = x / k

    return self.required_ebn0_ref_db + delta

ACM Policy

HysteresisACMPolicy

HysteresisACMPolicy(modcods, curves, target_bler, hysteresis_db=0.5, hold_time_s=2.0)

ACM policy with hysteresis to prevent ModCod flapping.

ModCods are sorted by required Eb/N0 (ascending = least demanding first). Step down (to lower ModCod) immediately when Eb/N0 drops below threshold. Step up (to higher ModCod) only when Eb/N0 exceeds threshold + hysteresis and hold time has elapsed since last switch.

Parameters:

Name Type Description Default
modcods list[ModCod]

Available ModCod configurations to select from.

required
curves dict[str, PerformanceCurve]

Mapping of ModCod name to its performance curve, used to look up the required Eb/N0 for each ModCod at the target BLER.

required
target_bler float

Target block error rate used to compute Eb/N0 thresholds.

required
hysteresis_db float

Additional Eb/N0 margin (in dB) required to step up to a higher ModCod, preventing rapid oscillation. Default is 0.5.

0.5
hold_time_s float

Minimum time (in seconds) that must elapse after the last ModCod switch before an upstep is permitted. Default is 2.0.

2.0
Source code in src/opensatcom/modem/acm.py
def __init__(
    self,
    modcods: list[ModCod],
    curves: dict[str, PerformanceCurve],
    target_bler: float,
    hysteresis_db: float = 0.5,
    hold_time_s: float = 2.0,
) -> None:
    # Sort by required Eb/N0 (ascending)
    self._modcods = sorted(
        modcods,
        key=lambda mc: curves[mc.name].required_ebn0_db(target_bler),
    )
    self._curves = curves
    self._target_bler = target_bler
    self._hysteresis_db = hysteresis_db
    self._hold_time_s = hold_time_s

    # Precompute thresholds
    self._thresholds = [
        curves[mc.name].required_ebn0_db(target_bler) + mc.impl_margin_db
        for mc in self._modcods
    ]

    self._current_idx = 0
    self._last_switch_t_s = -float("inf")

select_modcod

select_modcod(ebn0_db, t_s)

Select ModCod based on current Eb/N0 and time.

Parameters:

Name Type Description Default
ebn0_db float

Current measured Eb/N0 in dB.

required
t_s float

Current simulation time in seconds.

required

Returns:

Type Description
ModCod

The selected ModCod for the current link conditions.

Source code in src/opensatcom/modem/acm.py
def select_modcod(self, ebn0_db: float, t_s: float) -> ModCod:
    """Select ModCod based on current Eb/N0 and time.

    Parameters
    ----------
    ebn0_db : float
        Current measured Eb/N0 in dB.
    t_s : float
        Current simulation time in seconds.

    Returns
    -------
    ModCod
        The selected ModCod for the current link conditions.
    """
    # Immediate downstep if current ModCod can't be supported
    while (
        self._current_idx > 0
        and ebn0_db < self._thresholds[self._current_idx]
    ):
        self._current_idx -= 1
        self._last_switch_t_s = t_s

    # Try upstep with hysteresis + hold time
    time_since_switch = t_s - self._last_switch_t_s
    if time_since_switch >= self._hold_time_s:
        next_idx = self._current_idx + 1
        if next_idx < len(self._modcods):
            if ebn0_db >= self._thresholds[next_idx] + self._hysteresis_db:
                self._current_idx = next_idx
                self._last_switch_t_s = t_s

    return self._modcods[self._current_idx]

reset

reset()

Reset ACM state.

Returns:

Type Description
None
Source code in src/opensatcom/modem/acm.py
def reset(self) -> None:
    """Reset ACM state.

    Returns
    -------
    None
    """
    self._current_idx = 0
    self._last_switch_t_s = -float("inf")