All checks were successful
Build & Deploy Shannon / 🏗️ Build & Deploy Shannon (push) Successful in 3m1s
- Implemented bandwidth sensitivity and power sensitivity plots. - Created a contour map for bit rate multiplying factors. - Added input parameters for C/N and bandwidth with validation. - Displayed computed results and sensitivity analysis metrics. - Integrated interactive graphs for user exploration. - Included background information section for user guidance.
413 lines
17 KiB
Python
413 lines
17 KiB
Python
"""
|
|
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<br><sub>Ref: {hpa_power:.0f} W, "
|
|
f"{bandwidth:.0f} MHz, {br_rcv_higher:.1f} Mbps</sub>",
|
|
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])
|