Skip to content

About

Rust Crate for RF Systems Modeling

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

linkbudget

RF link budget analysis for satellite and terrestrial communication systems.

Crates.io Docs.rs

When To Use This Crate

Use linkbudget for end-to-end communication link questions: EIRP, FSPL, receiver G/T, C/No, SNR, Eb/No, BER, fade margin, modulation/FEC, PFD, Doppler, orbit geometry, receiver sensitivity, EVM, and quantization.

If the task starts from .sNp S-parameter files or network matrices, use touchstone. If it is an ordered hardware-block lineup with gain, NF, P1dB, or IP3 per stage, use gainlineup. Use rfconversions for standalone scalar dB, dBm, wavelength, and noise-temperature conversions.

Keep units explicit: powers are generally dBm or dBW as named, antenna gains are dBi, frequencies are Hz, distances are meters, C/No is dB-Hz, and SNR, Eb/No, and margins are dB.

For derivations, assumptions, worked checks, and links from equations to tests, start with the calculation guides.

Installation

[dependencies]
linkbudget = "0.7.0"

The receiver API below is available in 0.7.0. For the previous 0.6.5 API, see the 0.6.5 README.

Features

Module Description
Transmitter Output power, gain, EIRP (dBm/dBW)
Receiver Gain, explicit system or source noise, SNR, G/T
Path Loss Free space path loss (FSPL) from frequency and distance
Link Budget End-to-end: TX → path loss → RX → SNR → Eb/No → BER → margin
PHY Rate Shannon capacity from SNR and bandwidth
Orbits Slant range, circular orbit speed/period
Doppler Doppler shift, received frequency, max radial velocity
Power Flux Density PFD (dBW/m²) and PFD per MHz for regulatory analysis
Quantization ADC/DAC quantization SNR and ENOB
Modulation BPSK, QPSK, M-PSK, M-QAM, MSK — symbol rate, bandwidth, spectral efficiency
Energy (Eb/No) Eb/No, Es/No, Ec/No, C/No conversions between all energy-per-bit metrics
BER Theoretical BER curves (erfc/Q-function), required Eb/No, link margin
Sensitivity Receiver MDS from modulation, code rate, NF, target BER
EVM Error Vector Magnitude ↔ SNR conversions and margin checking
Coding (FEC) CodedModulation, FecCode enum, coding gain constants, DVB-S2 presets

Link Budget — End-to-End Example

use linkbudget::{LinkBudget, PathLoss, Transmitter, Receiver, ReceiverNoise, Modulation};
use linkbudget::coding;

let budget = LinkBudget {
    name: "Ka-band LEO downlink",
    bandwidth: 36e6,              // 36 MHz channel
    transmitter: Transmitter {
        output_power: 10.0,       // dBm
        gain: 35.0,               // dBi
        bandwidth: 36e6,          // Hz
    },
    receiver: Receiver {
        gain: 40.0,               // dBi
        noise: ReceiverNoise::SourceAndNoiseFigure {
            source_temperature_k: 290.0,
            noise_figure_db: 2.0, // receiver NF at the 290 K reference
        },
        bandwidth: 36e6,          // Hz
    },
    path_loss: PathLoss {
        frequency: 20.0e9,        // 20 GHz Ka-band
        distance: 550.0e3,        // 550 km LEO
    },
    frequency_dependent_loss: Some(3.0), // rain fade margin
};

// RF metrics
println!("EIRP: {:.1} dBm", budget.transmitter.eirp_dbm());
println!("G/T: {:.1} dB/K", budget.receiver.g_over_t_db());
println!("Path Loss: {:.1} dB", budget.path_loss());
println!("C/No: {:.1} dB·Hz", budget.c_over_no());

// Uncoded performance
println!("SNR: {:.1} dB", budget.snr());
println!("Eb/No (QPSK): {:.1} dB", budget.eb_no_db(&Modulation::Qpsk));
println!("BER (QPSK uncoded): {:.2e}", budget.ber(&Modulation::Qpsk));
println!("Shannon capacity: {:.1} Mbps", budget.phy_rate().mbps());

let margin = budget.link_margin_db(&Modulation::Qpsk, 1e-5).unwrap();
println!("Link margin (uncoded, BER=1e-5): {:.1} dB", margin);

// With FEC coding — DVB-S2 QPSK rate 3/4 (LDPC)
let coded = coding::dvbs2_qpsk_r34();
println!("BER (coded): {:.2e}", budget.ber_coded(&coded));
println!("Throughput: {:.0} Mbps", budget.throughput_bps(&coded) / 1e6);
println!("Coded margin: {:.1} dB",
    budget.link_margin_coded_db(&coded, 1e-5).unwrap());

Receiver Noise and Migration from 0.6.x

Replace the old Receiver.temperature and Receiver.noise_figure fields with Receiver.noise. Choose the variant that matches the temperature you have:

  • SystemTemperature accepts total input-referred system temperature. This already includes receiver-added noise. Do not apply noise figure again.
  • SourceAndNoiseFigure accepts source temperature and receiver noise figure. With F = 10^(NF/10), the total is T_sys = T_source + 290 * (F - 1) kelvin.

Use finite inputs with positive total temperature and positive noise bandwidth. Source temperature and receiver noise figure must be nonnegative. The model does not validate these inputs at runtime. Gain, signal power, and temperature must refer to the same receiver input plane.

For example, a 50 K source and noise factor 2 give a total of 340 K:

use linkbudget::{Receiver, ReceiverNoise};

let from_source = Receiver {
    gain: 40.0, // dBi
    noise: ReceiverNoise::SourceAndNoiseFigure {
        source_temperature_k: 50.0,
        noise_figure_db: 10.0 * 2.0_f64.log10(),
    },
    bandwidth: 20e6, // equivalent noise bandwidth, Hz
};
let from_system = Receiver {
    gain: 40.0,
    noise: ReceiverNoise::SystemTemperature { temperature_k: 340.0 },
    bandwidth: 20e6,
};
assert!((from_source.system_noise_temperature_k() - 340.0).abs() < 1e-10);
assert!((from_source.calculate_noise_power() - from_system.calculate_noise_power()).abs() < 1e-10);
assert!((from_source.g_over_t_db() - from_system.g_over_t_db()).abs() < 1e-10);

Noise power is k * T_sys * B_noise, and G/T uses the same T_sys. calculate_noise_floor() now returns total noise, as an alias of calculate_noise_power(). If you need source-only noise, compute `k * T_source

  • B_noise` from your source temperature.

For a 290 K source, SourceAndNoiseFigure preserves the old noise power and SNR. Its G/T now includes receiver-added noise. For other source temperatures, noise power changes because the old k * T_source * B_noise * F expression used the wrong noise-figure reference temperature.

LinkBudget::phy_rate() gives the ideal flat AWGN channel capacity: B_channel * log2(1 + C / (N0 * B_channel)). Receiver SNR uses the receiver noise bandwidth, which can differ from channel bandwidth. Capacity uses C/No to put signal and noise in the same channel bandwidth. This assumes all signal power is captured; it does not model filter clipping or distortion.

See the receiver noise decision and physics proof for the derivation, numerical counterexamples, and compatibility decision.

Modulation & BER

use linkbudget::{Modulation, ber};

let mod_qpsk = Modulation::Qpsk;

// Symbol rate and occupied bandwidth
let rs = mod_qpsk.symbol_rate(10e6, 0.75); // 10 Mbps, rate 3/4
let bw = mod_qpsk.occupied_bandwidth(rs, 0.35); // 35% roll-off
println!("Symbol rate: {:.2} Msps", rs / 1e6);
println!("Occupied BW: {:.2} MHz", bw / 1e6);

// BER at Eb/No = 10 dB
println!("BER: {:.2e}", ber::ber_from_db(10.0, &mod_qpsk));

// Required Eb/No for BER = 1e-6
println!("Required Eb/No: {:.1} dB",
    ber::required_eb_no_db(1e-6, &mod_qpsk).unwrap());

// Link margin: actual vs required
println!("Margin: {:.1} dB",
    ber::link_margin_db(12.0, 1e-6, &mod_qpsk).unwrap());

Energy-per-Bit Conversions

use linkbudget::energy;

// SNR = 20 dB in 10 MHz noise bandwidth
let c_no = energy::snr_to_c_over_no(20.0, 10e6);   // → 80 dB·Hz
let eb_no = energy::c_over_no_to_eb_over_no(c_no, 5e6); // 5 Mbps → 13 dB
println!("C/No: {:.1} dB·Hz", c_no);
println!("Eb/No: {:.1} dB", eb_no);

// Full chain: SNR → Eb/No via modulation and code rate
use linkbudget::Modulation;
let eb_no = energy::snr_to_eb_over_no(
    20.0,   // SNR (dB)
    10e6,   // noise bandwidth
    &Modulation::Qpsk,
    5e6,    // symbol rate
    0.75,   // code rate
);

Coded Modulation (FEC)

use linkbudget::coding::{self, CodedModulation, FecCode};
use linkbudget::Modulation;

// DVB-S2 preset
let cm = coding::dvbs2_qpsk_r34();
println!("{}", cm); // "QPSK + LDPC (R=0.75)"
println!("η = {:.2} bits/s/Hz", cm.spectral_efficiency());
println!("Throughput in 36 MHz: {:.0} Mbps", cm.throughput_bps(36e6) / 1e6);

// Required Eb/No with coding gain
println!("Required Eb/No: {:.1} dB", cm.required_eb_no_db(1e-5).unwrap());

// Link margin at 8 dB Eb/No
println!("Margin: {:.1} dB", cm.link_margin_db(8.0, 1e-5).unwrap());

// Custom: 16-QAM + Turbo rate 1/2
let custom = CodedModulation::new(
    Modulation::Mqam(16),
    FecCode::Turbo { rate: 0.5 },
);

Available DVB-S2 Presets

Preset Modulation FEC η (bits/s/Hz)
dvbs2_qpsk_r12() QPSK LDPC R=1/2 1.00
dvbs2_qpsk_r34() QPSK LDPC R=3/4 1.50
dvbs2_8psk_r23() 8-PSK LDPC R=2/3 2.00
dvbs2_16apsk_r34() 16-APSK LDPC R=3/4 3.00
dvbs2_32apsk_r56() 32-APSK LDPC R=5/6 4.17

Sensitivity

use linkbudget::{Modulation, sensitivity};

// Matched filter (root-raised-cosine) — ideal, α-independent
let matched = sensitivity::sensitivity_matched_filter_dbm(
    &Modulation::Qpsk, 10e6, 0.75, 3.0, 1e-6, 2.0,
).unwrap();
println!("Matched filter: {:.1} dBm", matched);

// Bandpass filter — practical, includes roll-off penalty
let bandpass = sensitivity::sensitivity_bandpass_dbm(
    &Modulation::Qpsk, 10e6, 0.75, 3.0, 1e-6, 2.0, 0.35,
).unwrap();
println!("Bandpass (α=0.35): {:.1} dBm", bandpass);

// Roll-off penalty
println!("Penalty: {:.2} dB", sensitivity::rolloff_penalty_db(0.35));

Roll-Off Factor (α)

The roll-off factor α controls the excess bandwidth of raised-cosine pulse shaping:

α Excess BW Sensitivity Penalty
0.00 0% (brick-wall, impractical) 0.00 dB
0.20 20% (DVB-S2) 0.79 dB
0.25 25% 0.97 dB
0.35 35% (legacy DVB-S) 1.30 dB
0.50 50% 1.76 dB
1.00 100% 3.01 dB

With a matched filter (RRC at TX + RX), noise bandwidth = symbol rate regardless of α, so sensitivity is unaffected. The penalty applies when using a simple bandpass filter set to the occupied bandwidth Rs×(1+α).

EVM

use linkbudget::evm;

// EVM ↔ SNR
println!("EVM at 25 dB SNR: {:.1}%", evm::evm_percent_from_snr_db(25.0));
println!("SNR for 5% EVM: {:.1} dB", evm::snr_db_from_evm_percent(5.0));

// Pass/fail check with margin
let (pass, margin) = evm::evm_margin(5.0, 8.0); // measured 5%, required 8%
println!("Pass: {}, margin: {:.1} dB", pass, margin);

Doppler

use linkbudget::doppler;

let shift = doppler::doppler_shift_hz(14e9, 7000.0); // 14 GHz, 7 km/s
println!("Doppler shift: {:.0} Hz", shift); // ~327 kHz

let received = doppler::doppler_received_frequency(14e9, 7000.0);
println!("Received freq: {:.6} GHz", received / 1e9);

Power Flux Density

use linkbudget::{power_flux_density_dbw_per_m2, pfd_per_mhz};

let pfd = power_flux_density_dbw_per_m2(45.0, 550e3);
println!("PFD: {:.1} dBW/m²", pfd);

let pfd_mhz = pfd_per_mhz(45.0, 550e3, 36.0);
println!("PFD/MHz: {:.1} dBW/m²/MHz", pfd_mhz);

Quantization

use linkbudget::{quantization_snr_db, enob_from_snr};

println!("12-bit SNR: {:.2} dB", quantization_snr_db(12)); // 74.00
println!("ENOB at 65 dB: {:.1} bits", enob_from_snr(65.0)); // 10.5

CLI

linkbudget          # runs the built-in example

Diagnostics (Tracing)

linkbudget uses tracing for structured, runtime-controllable diagnostics. Set the RUST_LOG environment variable to see what the CLI is doing:

# See HTML generation and browser launch info
RUST_LOG=linkbudget=info linkbudget config.toml

# See path resolution, file URL construction, and all diagnostics
RUST_LOG=linkbudget=debug linkbudget config.toml

# Only warnings and errors (quiet mode)
RUST_LOG=linkbudget=warn linkbudget config.toml

If you use linkbudget as a library, install any tracing subscriber in your application to capture events. Without a subscriber, all tracing calls are zero-cost no-ops.

References


Optional: Monte Carlo Receiver Margin Example (examples crate)

This repo includes an optional standalone example crate for Monte Carlo analysis of receiver SNR margin using montycarlo + linkbudget::Receiver.

Path:

  • examples/montecarlo-cn0-target/

What it does:

  • Randomizes realistic receiver parameters each trial:
    • source noise temperature (K)
    • receiver noise figure at the 290 K reference (dB)
    • bandwidth (Hz)
    • input power (dBm)
  • Instantiates a Receiver and computes SNR per trial
  • Computes SNR margin vs a target and writes CSV + summary
  • Produces Python histogram/CDF plots

Run:

cd examples/montecarlo-cn0-target
uv sync
cargo run
uv run plot_cn0_margin.py

Output files:

  • output/receiver_snr_margin_samples.csv
  • output/receiver_snr_margin_summary.txt
  • output/receiver_snr_margin_plots.png

About

Rust Crate for RF Systems Modeling

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages