""" 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])