"""
Page 2: Shannon and Friends in the Real World
Complete satellite link budget with interactive Plotly graphs.
"""
import streamlit as st
import numpy as np
import plotly.graph_objects as go
from math import log
from core.calculations import (
combine_cnr,
shannon_capacity,
compute_satellite_link,
compute_receiver,
compute_baseband,
fmt_br,
fmt_power,
fmt_gain,
fmt_pfd,
fmt_psd,
fmt_ploss,
)
from core.help_texts import REAL_WORLD_HELP
def _make_bw_sensitivity_real(
cnr_nyq, bandwidth, rolloff, overheads, penalties, cnr_imp, sat_cir, hpa_power
) -> go.Figure:
n = 40
bw = np.zeros(n)
br = np.zeros(n)
cnr = np.zeros(n)
cnr[0] = cnr_nyq + 10 * log(8, 10)
bw[0] = bandwidth / 8
cnr_rcv_0 = combine_cnr(cnr[0], cnr_imp, sat_cir)
br[0] = shannon_capacity(bw[0] / (1 + rolloff / 100), cnr_rcv_0, penalties) / (1 + overheads / 100)
for i in range(1, n):
bw[i] = bw[i - 1] * 2 ** (1 / 6)
cnr[i] = cnr[i - 1] - 10 * log(bw[i] / bw[i - 1], 10)
cnr_rcv_i = combine_cnr(cnr[i], cnr_imp, sat_cir)
br[i] = shannon_capacity(bw[i] / (1 + rolloff / 100), cnr_rcv_i, penalties) / (1 + overheads / 100)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=bw, y=br, mode="lines",
name="Higher Layers BR",
line=dict(color="#4FC3F7", width=3),
))
# Reference point
cnr_rcv_ref = combine_cnr(cnr_nyq, cnr_imp, sat_cir)
br_ref = shannon_capacity(bandwidth / (1 + rolloff / 100), cnr_rcv_ref, penalties) / (1 + overheads / 100)
fig.add_trace(go.Scatter(
x=[bandwidth], y=[br_ref], mode="markers+text",
name=f"Ref: {bandwidth:.0f} MHz, {br_ref:.1f} Mbps",
marker=dict(size=12, color="#FF7043", symbol="diamond"),
text=[f"{br_ref:.1f} Mbps"],
textposition="top center",
))
fig.update_layout(
title=f"Higher Layers Bit Rate at Constant HPA Power: {hpa_power:.1f} W",
xaxis_title="Occupied Bandwidth [MHz]",
yaxis_title="Bit Rate [Mbps]",
template="plotly_dark",
height=500,
)
return fig
def _make_power_sensitivity_real(
cnr_nyq, bw_nyq, hpa_power, overheads, penalties, cnr_imp, sat_cir, bandwidth
) -> go.Figure:
n = 40
power = np.zeros(n)
br = np.zeros(n)
cnr = np.zeros(n)
power[0] = hpa_power / 8
cnr[0] = cnr_nyq - 10 * log(8, 10)
cnr_rcv_i = combine_cnr(cnr[0], cnr_imp, sat_cir)
br[0] = shannon_capacity(bw_nyq, cnr_rcv_i, penalties) / (1 + overheads / 100)
for i in range(1, n):
power[i] = power[i - 1] * 2 ** (1 / 6)
cnr[i] = cnr[i - 1] + 10 * log(2 ** (1 / 6), 10)
cnr_rcv_i = combine_cnr(cnr[i], cnr_imp, sat_cir)
br[i] = shannon_capacity(bw_nyq, cnr_rcv_i, penalties) / (1 + overheads / 100)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=power, y=br, mode="lines",
name="Higher Layers BR",
line=dict(color="#81C784", width=3),
))
cnr_rcv_ref = combine_cnr(cnr_nyq, cnr_imp, sat_cir)
br_ref = shannon_capacity(bw_nyq, cnr_rcv_ref, penalties) / (1 + overheads / 100)
fig.add_trace(go.Scatter(
x=[hpa_power], y=[br_ref], mode="markers+text",
name=f"Ref: {hpa_power:.0f} W, {br_ref:.1f} Mbps",
marker=dict(size=12, color="#FF7043", symbol="diamond"),
text=[f"{br_ref:.1f} Mbps"],
textposition="top center",
))
fig.update_layout(
title=f"Higher Layers Bit Rate at Constant BW: {bandwidth:.1f} MHz",
xaxis_title="HPA Output Power [W]",
yaxis_title="Bit Rate [Mbps]",
template="plotly_dark",
height=500,
)
return fig
def _make_br_factor_map_real(
cnr_nyq, bw_nyq, rolloff, overheads, penalties, cnr_imp, sat_cir,
hpa_power, bandwidth, br_rcv_higher,
) -> go.Figure:
n = 41
bw_mul = np.zeros((n, n))
p_mul = np.zeros((n, n))
br_mul = np.zeros((n, n))
for i in range(n):
for j in range(n):
bw_mul[i, j] = (i + 1) / 8
p_mul[i, j] = (j + 1) / 8
cnr_ij = cnr_nyq + 10 * log(p_mul[i, j] / bw_mul[i, j], 10)
cnr_rcv_ij = combine_cnr(cnr_ij, cnr_imp, sat_cir)
bw_ij = bw_nyq * bw_mul[i, j]
br_ij = shannon_capacity(bw_ij / (1 + rolloff / 100), cnr_rcv_ij, penalties) / (1 + overheads / 100)
br_mul[i, j] = br_ij / br_rcv_higher if br_rcv_higher > 0 else 0
fig = go.Figure(data=go.Contour(
z=br_mul,
x=bw_mul[:, 0],
y=p_mul[0, :],
colorscale="Viridis",
contours=dict(showlabels=True, labelfont=dict(size=10, color="white")),
colorbar=dict(title="BR Factor"),
))
fig.update_layout(
title=f"BR Multiplying Factor
Ref: {hpa_power:.0f} W, "
f"{bandwidth:.0f} MHz, {br_rcv_higher:.1f} Mbps",
xaxis_title="Bandwidth Factor",
yaxis_title="Power Factor",
template="plotly_dark",
height=550,
)
return fig
def render():
"""Render the Real World satellite link budget page."""
# ββ Header ββ
col_img, col_title = st.columns([1, 3])
with col_img:
st.image("Satellite.png", width=200)
with col_title:
st.markdown("# π°οΈ Shannon & Friends in the Real World")
st.markdown("From theory to satellite communication link budget.")
wiki_cols = st.columns(4)
wiki_cols[0].link_button("π Harry Nyquist", "https://en.wikipedia.org/wiki/Harry_Nyquist")
wiki_cols[1].link_button("π Richard Hamming", "https://en.wikipedia.org/wiki/Richard_Hamming")
wiki_cols[2].link_button("π Andrew Viterbi", "https://en.wikipedia.org/wiki/Andrew_Viterbi")
wiki_cols[3].link_button("π Claude Berrou", "https://en.wikipedia.org/wiki/Claude_Berrou")
st.divider()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 1: Satellite Link
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown("## π‘ Satellite Link")
col1, col2, col3 = st.columns(3)
with col1:
sat_alt = st.number_input("Satellite Altitude [km]", value=35786.0, step=100.0,
help=REAL_WORLD_HELP["sat_alt"])
sat_lat = st.number_input("Satellite Latitude [Β°]", value=0.0, step=0.1,
help=REAL_WORLD_HELP["sat_latlon"])
sat_lon = st.number_input("Satellite Longitude [Β°]", value=19.2, step=0.1,
help=REAL_WORLD_HELP["sat_latlon"])
with col2:
gs_lat = st.number_input("Ground Station Latitude [Β°]", value=49.7, step=0.1,
help=REAL_WORLD_HELP["gs_latlon"])
gs_lon = st.number_input("Ground Station Longitude [Β°]", value=6.3, step=0.1,
help=REAL_WORLD_HELP["gs_latlon"])
freq = st.number_input("Frequency [GHz]", value=12.0, min_value=0.1, step=0.5,
help=REAL_WORLD_HELP["freq"])
with col3:
hpa_power = st.number_input("HPA Output Power [W]", value=120.0, min_value=0.1, step=10.0,
help=REAL_WORLD_HELP["hpa"])
sat_loss = st.number_input("Output Losses [dB]", value=2.0, min_value=0.0, step=0.5,
help=REAL_WORLD_HELP["losses"])
availability = st.number_input("Link Availability [%]", value=99.9,
min_value=90.0, max_value=99.999, step=0.1,
help=REAL_WORLD_HELP["availability"])
col4, col5 = st.columns(2)
with col4:
sat_cir_input = st.text_input("TX Impairments C/I [dB] (comma-sep)", value="25, 25",
help=REAL_WORLD_HELP["sat_cir"])
sat_beam = st.number_input("Beam Diameter [Β°]", value=3.0, min_value=0.1, step=0.1,
help=REAL_WORLD_HELP["sat_beam"])
with col5:
gain_offset = st.number_input("Offset from Peak [dB]", value=0.0, min_value=0.0, step=0.5,
help=REAL_WORLD_HELP["gain_offset"])
# Parse satellite C/I
try:
sat_cir_list = [float(v.strip()) for v in sat_cir_input.split(",")]
except ValueError:
st.error("β Invalid C/I values.")
return
# Compute satellite link
try:
with st.spinner("Computing atmospheric attenuation (ITU-R model)..."):
sat = compute_satellite_link(
freq, hpa_power, sat_loss, sat_cir_list, sat_beam, gain_offset,
sat_alt, sat_lat, sat_lon, gs_lat, gs_lon, availability,
)
except Exception as e:
st.error(f"β Satellite link computation error: {e}")
return
# Display satellite link results
st.markdown("#### π Satellite Link Results")
r1, r2, r3 = st.columns(3)
r1.metric("Output Power", fmt_power(sat["sig_power"]), help=REAL_WORLD_HELP["output_power"])
r2.metric("Antenna Gain", fmt_gain(sat["sat_gain_linear"]), help=REAL_WORLD_HELP["sat_gain"])
r3.metric("EIRP", fmt_power(sat["eirp_linear"]), help=REAL_WORLD_HELP["eirp"])
r4, r5, r6 = st.columns(3)
r4.metric("Path Length", f"{sat['path_length']:.1f} km @ {sat['elevation']:.1f}Β°",
help=REAL_WORLD_HELP["path_length"])
r5.metric("Atmospheric Loss", f"{sat['atm_loss_db']:.1f} dB",
help=REAL_WORLD_HELP["atm_loss"])
r6.metric("Path Dispersion", fmt_ploss(sat["path_loss_linear"]),
help=REAL_WORLD_HELP["path_loss"])
st.metric("Power Flux Density", fmt_pfd(sat["pfd_linear"]), help=REAL_WORLD_HELP["pfd"])
if sat["elevation"] <= 0:
st.warning("β οΈ Satellite is below the horizon (negative elevation). Results may not be meaningful.")
st.divider()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 2: Radio Front End
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown("## π» Radio Front End")
col_r1, col_r2 = st.columns(2)
with col_r1:
cpe_ant_d = st.number_input("Receive Antenna Size [m]", value=0.6, min_value=0.1, step=0.1,
help=REAL_WORLD_HELP["cpe_ant"])
with col_r2:
cpe_t_clear = st.number_input("Noise Temperature [K]", value=120.0, min_value=10.0, step=10.0,
help=REAL_WORLD_HELP["cpe_temp"])
try:
rcv = compute_receiver(
sat["pfd_linear"], sat["atm_loss_db"], sat["wavelength"],
cpe_ant_d, cpe_t_clear,
)
except Exception as e:
st.error(f"β Receiver computation error: {e}")
return
st.markdown("#### π Receiver Results")
rx1, rx2 = st.columns(2)
rx1.metric("Antenna Area Β· G/T", f"{rcv['cpe_ae']:.2f} mΒ² Β· {rcv['cpe_g_t']:.1f} dB/K",
help=REAL_WORLD_HELP["cpe_gain"])
rx2.metric("RX Power (C)", fmt_power(rcv["rx_power"]), help=REAL_WORLD_HELP["rx_power"])
rx3, rx4 = st.columns(2)
rx3.metric("Nβ", fmt_psd(rcv["n0"] * 1e6), help=REAL_WORLD_HELP["n0"])
rx4.metric("BR at β BW", fmt_br(rcv["br_infinity"]), help=REAL_WORLD_HELP["br_inf"])
rx5, rx6 = st.columns(2)
rx5.metric("BR at SpEff=1", fmt_br(rcv["br_spe_1"]), help=REAL_WORLD_HELP["br_unit"])
rx6.metric("BR at SpEff=2", fmt_br(rcv["br_spe_double"]), help=REAL_WORLD_HELP["br_double"])
st.divider()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 3: Baseband Unit
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown("## π» Baseband Unit")
col_b1, col_b2, col_b3 = st.columns(3)
with col_b1:
bandwidth = st.number_input("Occupied Bandwidth [MHz]", value=36.0, min_value=0.1, step=1.0,
help=REAL_WORLD_HELP["bandwidth"], key="bw_real")
rolloff = st.number_input("Nyquist Rolloff [%]", value=5.0, min_value=0.0, max_value=100.0, step=1.0,
help=REAL_WORLD_HELP["rolloff"])
with col_b2:
cir_input = st.text_input("RX Impairments C/I [dB]", value="20",
help=REAL_WORLD_HELP["cir"])
penalties = st.number_input("Implementation Penalty [dB]", value=1.5, min_value=0.0, step=0.1,
help=REAL_WORLD_HELP["penalty"])
with col_b3:
overheads = st.number_input("Higher Layers Overhead [%]", value=5.0, min_value=0.0, step=1.0,
help=REAL_WORLD_HELP["overhead"])
try:
cnr_imp_list = [float(v.strip()) for v in cir_input.split(",")]
except ValueError:
st.error("β Invalid C/I values.")
return
try:
bb = compute_baseband(
rcv["c_n0_mhz"], rcv["br_infinity"], rcv["bw_spe_1"],
sat["sat_cir"], bandwidth, rolloff, overheads, cnr_imp_list, penalties,
)
except Exception as e:
st.error(f"β Baseband computation error: {e}")
return
st.markdown("#### π Baseband Results")
b1, b2, b3 = st.columns(3)
b1.metric("SNR in Available BW", f"{bb['cnr_bw']:.1f} dB in {bandwidth:.1f} MHz",
help=REAL_WORLD_HELP["cnr_bw"])
b2.metric("SNR in Nyquist BW", f"{bb['cnr_nyq']:.1f} dB in {bb['bw_nyq']:.1f} MHz",
help=REAL_WORLD_HELP["cnr_nyq"])
b3.metric("SNR at Receiver", f"{bb['cnr_rcv']:.1f} dB",
help=REAL_WORLD_HELP["cnr_rcv"])
st.divider()
b4, b5 = st.columns(2)
with b4:
st.markdown("##### π― Theoretical")
st.metric(
"Theoretical BR",
f"{fmt_br(bb['br_nyq'])} Β· {bb['br_nyq_norm']:.0%}",
help=REAL_WORLD_HELP["br_nyq"],
)
st.caption(f"Spectral Eff: {bb['spe_nyq']:.2f} bps/Hz Β· {bb['bits_per_symbol']:.2f} b/Symbol")
with b5:
st.markdown("##### π Practical")
st.metric(
"Physical Layer BR",
f"{fmt_br(bb['br_rcv'])} Β· {bb['br_rcv_norm']:.0%}",
help=REAL_WORLD_HELP["br_rcv"],
)
st.metric(
"Higher Layers BR",
f"{fmt_br(bb['br_rcv_higher'])} Β· {bb['br_rcv_h_norm']:.0%}",
delta=f"{bb['spe_higher']:.2f} bps/Hz",
help=REAL_WORLD_HELP["br_high"],
)
st.divider()
# ββ Graphs ββ
st.markdown("### π Interactive Graphs")
tab_bw, tab_pow, tab_map = st.tabs([
"πΆ BW Sensitivity",
"β‘ Power Sensitivity",
"πΊοΈ BR Factor Map",
])
cnr_imp = combine_cnr(*cnr_imp_list)
with tab_bw:
st.plotly_chart(
_make_bw_sensitivity_real(
bb["cnr_nyq"], bandwidth, rolloff, overheads, penalties,
cnr_imp, sat["sat_cir"], hpa_power,
),
width="stretch",
)
with tab_pow:
st.plotly_chart(
_make_power_sensitivity_real(
bb["cnr_nyq"], bb["bw_nyq"], hpa_power, overheads, penalties,
cnr_imp, sat["sat_cir"], bandwidth,
),
width="stretch",
)
with tab_map:
st.plotly_chart(
_make_br_factor_map_real(
bb["cnr_nyq"], bb["bw_nyq"], rolloff, overheads, penalties,
cnr_imp, sat["sat_cir"], hpa_power, bandwidth, bb["br_rcv_higher"],
),
width="stretch",
)
# ββ Help ββ
with st.expander("π Background Information"):
help_topic = st.selectbox(
"Choose a topic:",
options=["satellite", "advanced", "help"],
format_func=lambda x: {
"satellite": "π°οΈ Link Budget Overview",
"advanced": "π§ Advanced Notes",
"help": "β How to use",
}[x],
key="help_real",
)
st.markdown(REAL_WORLD_HELP[help_topic])