feat: add interactive exploration of Shannon's capacity formula with Plotly graphs
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.
This commit is contained in:
Poidevin, Antoine (ITOP CM) - AF
2026-02-20 10:33:09 +01:00
parent beda405953
commit 6a4ccc3376
38 changed files with 4319 additions and 11161 deletions

View File

@@ -0,0 +1,762 @@
"""
Interactive Satellite Link Animation
=====================================
HTML5 Canvas animation showing a satellite communicating with a ground station,
with toggleable impairment layers (atmosphere, rain, free-space loss, noise…).
"""
import streamlit as st
import streamlit.components.v1 as components
_ANIMATION_HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: transparent; overflow: hidden; font-family: 'Segoe UI', system-ui, sans-serif; }
canvas { display: block; }
/* ── Control Panel ── */
#controls {
position: absolute; top: 14px; right: 18px;
background: rgba(13,27,42,0.92); border: 1px solid rgba(79,195,247,0.3);
border-radius: 14px; padding: 16px 18px; min-width: 220px;
backdrop-filter: blur(12px); box-shadow: 0 8px 32px rgba(0,0,0,0.45);
}
#controls h3 {
color: #4FC3F7; font-size: 13px; text-transform: uppercase;
letter-spacing: 1.5px; margin-bottom: 12px; text-align: center;
}
.toggle-row {
display: flex; align-items: center; justify-content: space-between;
margin: 7px 0; cursor: pointer; padding: 4px 6px; border-radius: 8px;
transition: background 0.2s;
}
.toggle-row:hover { background: rgba(79,195,247,0.08); }
.toggle-row label { color: #cbd5e1; font-size: 12.5px; cursor: pointer; flex: 1; }
.toggle-row .dot {
width: 10px; height: 10px; border-radius: 50%; margin-right: 10px; flex-shrink: 0;
}
/* Toggle switch */
.switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute; cursor: pointer; inset: 0;
background: #334155; border-radius: 20px; transition: 0.3s;
}
.slider::before {
content: ""; position: absolute; height: 14px; width: 14px;
left: 3px; bottom: 3px; background: #94a3b8;
border-radius: 50%; transition: 0.3s;
}
.switch input:checked + .slider { background: #0f3460; }
.switch input:checked + .slider::before { transform: translateX(16px); background: #4FC3F7; }
/* Legend box */
#legend {
position: absolute; bottom: 14px; left: 18px;
background: rgba(13,27,42,0.88); border: 1px solid rgba(79,195,247,0.2);
border-radius: 12px; padding: 14px 16px; max-width: 340px;
backdrop-filter: blur(10px); color: #e2e8f0; font-size: 12px; line-height: 1.6;
box-shadow: 0 4px 20px rgba(0,0,0,0.35);
transition: opacity 0.4s;
}
#legend h4 { color: #4FC3F7; margin-bottom: 6px; font-size: 13px; }
#legend .info-text { color: #94a3b8; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<!-- Control toggles -->
<div id="controls">
<h3>⚡ Impairments</h3>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#ff6b6b"></span>
<label>Free-Space Loss</label>
<label class="switch"><input type="checkbox" id="tFSL" checked><span class="slider"></span></label>
</div>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#ffd93d"></span>
<label>Atmospheric Attn.</label>
<label class="switch"><input type="checkbox" id="tAtm" checked><span class="slider"></span></label>
</div>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#6bcbff"></span>
<label>Rain Attenuation</label>
<label class="switch"><input type="checkbox" id="tRain" checked><span class="slider"></span></label>
</div>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#c084fc"></span>
<label>Ionospheric Effects</label>
<label class="switch"><input type="checkbox" id="tIono" checked><span class="slider"></span></label>
</div>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#fb923c"></span>
<label>Thermal Noise</label>
<label class="switch"><input type="checkbox" id="tNoise" checked><span class="slider"></span></label>
</div>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#34d399"></span>
<label>Pointing Loss</label>
<label class="switch"><input type="checkbox" id="tPoint" checked><span class="slider"></span></label>
</div>
</div>
<!-- Info legend -->
<div id="legend">
<h4 id="legendTitle">📡 Satellite Link Overview</h4>
<span id="legendText" class="info-text">
Toggle each impairment to see how it affects the signal.
<b style="color:#4FC3F7">Cyan</b> = Downlink (sat → ground).
<b style="color:#34d399">Green</b> = Uplink (ground → sat).
The signal strength bar shows the cumulative effect.
</span>
</div>
<script>
// ── Canvas setup ──
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H;
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// ── Toggle states ──
const ids = ['tFSL','tAtm','tRain','tIono','tNoise','tPoint'];
function isOn(id) { return document.getElementById(id).checked; }
// Legend descriptions
const legendData = {
tFSL: { title: '📉 Free-Space Path Loss (FSPL)',
text: 'Signal power decreases with the square of distance (1/r²). At 36,000 km (GEO), FSPL ≈ 200 dB at Ku-band. This is the dominant loss in any satellite link.' },
tAtm: { title: '🌫️ Atmospheric Attenuation',
text: 'Oxygen and water vapour molecules absorb RF energy. Attenuation increases with frequency: ~0.2 dB at C-band to >1 dB at Ka-band for a 30° elevation.' },
tRain: { title: '🌧️ Rain Attenuation',
text: 'Raindrops scatter and absorb signals, especially above 10 GHz. A heavy storm can add 1020 dB of loss at Ka-band. ITU-R P.618 models this statistically.' },
tIono: { title: '🔮 Ionospheric Effects',
text: 'The ionosphere causes Faraday rotation, scintillation and group delay. Effects are strongest below 3 GHz and vary with solar activity (11-year cycle).' },
tNoise: { title: '🌡️ Thermal Noise',
text: 'Every component adds noise (N = kTB). The system noise temperature combines antenna noise, LNB noise figure, and sky temperature. Lower C/N = fewer bits/s.' },
tPoint: { title: '🎯 Pointing Loss',
text: 'Misalignment between the antenna boresight and satellite direction. A 0.1° error on a 1.2 m dish at Ku-band ≈ 0.5 dB loss. Wind and thermal expansion are causes.' },
};
// Hover / click detection on toggles
ids.forEach(id => {
const row = document.getElementById(id).closest('.toggle-row');
row.addEventListener('mouseenter', () => {
const d = legendData[id];
document.getElementById('legendTitle').textContent = d.title;
document.getElementById('legendText').textContent = d.text;
});
row.addEventListener('mouseleave', () => {
document.getElementById('legendTitle').textContent = '📡 Satellite Link Overview';
document.getElementById('legendText').innerHTML = 'Toggle each impairment to see how it affects the signal. <b style=\"color:#4FC3F7\">Cyan</b> = Downlink (sat → ground). <b style=\"color:#34d399\">Green</b> = Uplink (ground → sat). The signal strength bar shows the cumulative effect.';
});
});
// ── Stars ──
const stars = Array.from({length: 200}, () => ({
x: Math.random(), y: Math.random() * 0.65,
r: Math.random() * 1.3 + 0.3,
twinkle: Math.random() * Math.PI * 2,
speed: 0.3 + Math.random() * 1.5,
}));
// ── Rain drops ──
const drops = Array.from({length: 120}, () => ({
x: Math.random(), y: Math.random(),
len: 8 + Math.random() * 18,
speed: 4 + Math.random() * 6,
opacity: 0.15 + Math.random() * 0.35,
}));
// ── Animation state ──
let t = 0;
const satOrbitAngle = { v: 0 };
// ── Signal packets (fixed pool, bidirectional) ──
const MAX_DOWN = 8; // downlink packets (sat → ground)
const MAX_UP = 5; // uplink packets (ground → sat)
let downPackets = [];
let upPackets = [];
function initPackets() {
downPackets = Array.from({length: MAX_DOWN}, (_, i) => ({
progress: i / MAX_DOWN, // spread evenly
speed: 0.003 + Math.random() * 0.002,
}));
upPackets = Array.from({length: MAX_UP}, (_, i) => ({
progress: i / MAX_UP,
speed: 0.0025 + Math.random() * 0.002,
}));
}
initPackets();
// ── Drawing helpers ──
function lerp(a, b, t) { return a + (b - a) * t; }
function drawEarth() {
const earthY = H * 0.88;
const earthR = W * 0.9;
// Atmosphere glow
const atmGrad = ctx.createRadialGradient(W/2, earthY + earthR*0.3, earthR * 0.85, W/2, earthY + earthR*0.3, earthR * 1.15);
atmGrad.addColorStop(0, 'rgba(56,189,248,0.08)');
atmGrad.addColorStop(0.5, 'rgba(56,189,248,0.03)');
atmGrad.addColorStop(1, 'transparent');
ctx.fillStyle = atmGrad;
ctx.fillRect(0, 0, W, H);
// Earth body
const grad = ctx.createRadialGradient(W/2, earthY + earthR*0.3, earthR * 0.2, W/2, earthY + earthR*0.3, earthR);
grad.addColorStop(0, '#1a6b4a');
grad.addColorStop(0.4, '#0f4c75');
grad.addColorStop(0.8, '#0b3d6b');
grad.addColorStop(1, '#062a4d');
ctx.beginPath();
ctx.arc(W/2, earthY + earthR * 0.3, earthR, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
// Atmosphere rim
if (isOn('tAtm')) {
ctx.beginPath();
ctx.arc(W/2, earthY + earthR * 0.3, earthR + 8, Math.PI, 2 * Math.PI);
ctx.strokeStyle = 'rgba(56,189,248,0.25)';
ctx.lineWidth = 18;
ctx.stroke();
ctx.beginPath();
ctx.arc(W/2, earthY + earthR * 0.3, earthR + 25, Math.PI * 1.1, Math.PI * 1.9);
ctx.strokeStyle = 'rgba(255,217,61,0.12)';
ctx.lineWidth = 12;
ctx.stroke();
// Label
ctx.fillStyle = 'rgba(255,217,61,0.7)';
ctx.font = '11px system-ui';
ctx.fillText('Troposphere', W/2 + earthR * 0.25, earthY - 30);
}
// Ionosphere
if (isOn('tIono')) {
for (let i = 0; i < 3; i++) {
ctx.beginPath();
ctx.arc(W/2, earthY + earthR * 0.3, earthR + 55 + i * 18, Math.PI * 1.05, Math.PI * 1.95);
ctx.strokeStyle = `rgba(192,132,252,${0.08 + Math.sin(t * 0.8 + i) * 0.05})`;
ctx.lineWidth = 2;
ctx.stroke();
}
ctx.fillStyle = 'rgba(192,132,252,0.6)';
ctx.font = '11px system-ui';
ctx.fillText('Ionosphere', W/2 - earthR * 0.35, earthY - 85);
}
return earthY;
}
function drawGroundStation(earthY) {
const gx = W * 0.38;
const gy = earthY - 18;
// Dish base
ctx.fillStyle = '#475569';
ctx.fillRect(gx - 4, gy - 5, 8, 20);
// Dish
ctx.beginPath();
ctx.ellipse(gx, gy - 12, 22, 28, -0.4, -Math.PI * 0.5, Math.PI * 0.5);
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 3;
ctx.stroke();
// Feed
ctx.beginPath();
ctx.moveTo(gx, gy - 12);
ctx.lineTo(gx + 18, gy - 30);
ctx.strokeStyle = '#64748b';
ctx.lineWidth = 2;
ctx.stroke();
// LNB
ctx.beginPath();
ctx.arc(gx + 18, gy - 31, 4, 0, Math.PI * 2);
ctx.fillStyle = '#f59e0b';
ctx.fill();
// Label
ctx.fillStyle = '#94a3b8';
ctx.font = 'bold 11px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Ground Station', gx, gy + 28);
ctx.textAlign = 'start';
return { x: gx + 18, y: gy - 31 };
}
function drawSatellite() {
const cx = W * 0.62;
const cy = H * 0.12;
const bob = Math.sin(t * 0.5) * 4;
const sx = cx;
const sy = cy + bob;
// Solar panels
ctx.fillStyle = '#1e3a5f';
ctx.strokeStyle = '#38bdf8';
ctx.lineWidth = 1;
for (const side of [-1, 1]) {
ctx.save();
ctx.translate(sx + side * 22, sy);
ctx.fillRect(side > 0 ? 4 : -38, -14, 34, 28);
ctx.strokeRect(side > 0 ? 4 : -38, -14, 34, 28);
// Panel lines
for (let i = 1; i < 4; i++) {
ctx.beginPath();
ctx.moveTo(side > 0 ? 4 + i * 8.5 : -38 + i * 8.5, -14);
ctx.lineTo(side > 0 ? 4 + i * 8.5 : -38 + i * 8.5, 14);
ctx.strokeStyle = 'rgba(56,189,248,0.3)';
ctx.stroke();
}
ctx.restore();
}
// Body
ctx.fillStyle = '#334155';
ctx.strokeStyle = '#64748b';
ctx.lineWidth = 1.5;
ctx.fillRect(sx - 12, sy - 16, 24, 32);
ctx.strokeRect(sx - 12, sy - 16, 24, 32);
// Antenna dish
ctx.beginPath();
ctx.ellipse(sx, sy + 22, 10, 5, 0, 0, Math.PI);
ctx.fillStyle = '#94a3b8';
ctx.fill();
// Antenna feed
ctx.beginPath();
ctx.moveTo(sx, sy + 16);
ctx.lineTo(sx, sy + 28);
ctx.strokeStyle = '#cbd5e1';
ctx.lineWidth = 2;
ctx.stroke();
// Status LED
ctx.beginPath();
ctx.arc(sx, sy - 10, 3, 0, Math.PI * 2);
ctx.fillStyle = `rgba(52,211,153,${0.5 + Math.sin(t * 2) * 0.5})`;
ctx.fill();
// Label
ctx.fillStyle = '#94a3b8';
ctx.font = 'bold 11px system-ui';
ctx.textAlign = 'center';
ctx.fillText('GEO Satellite', sx, sy - 34);
ctx.font = '10px system-ui';
ctx.fillStyle = '#64748b';
ctx.fillText('36 000 km', sx, sy - 22);
ctx.textAlign = 'start';
return { x: sx, y: sy + 28 };
}
function drawSignalBeam(from, to) {
// Count active impairments
const active = ids.filter(isOn).length;
const strength = Math.max(0.15, 1 - active * 0.12);
const dx = to.x - from.x;
const dy = to.y - from.y;
const len = Math.sqrt(dx*dx + dy*dy);
const nx = -dy / len;
const ny = dx / len;
const spreadTop = 8;
const spreadBot = 45;
// ── Downlink beam cone (sat → ground) ──
const beamGradDown = ctx.createLinearGradient(from.x, from.y, to.x, to.y);
beamGradDown.addColorStop(0, `rgba(79,195,247,${0.20 * strength})`);
beamGradDown.addColorStop(0.5, `rgba(79,195,247,${0.08 * strength})`);
beamGradDown.addColorStop(1, `rgba(79,195,247,${0.03 * strength})`);
ctx.beginPath();
ctx.moveTo(from.x + nx * spreadTop, from.y + ny * spreadTop);
ctx.lineTo(to.x + nx * spreadBot, to.y + ny * spreadBot);
ctx.lineTo(to.x - nx * spreadBot * 0.3, to.y - ny * spreadBot * 0.3);
ctx.lineTo(from.x - nx * spreadTop * 0.3, from.y - ny * spreadTop * 0.3);
ctx.closePath();
ctx.fillStyle = beamGradDown;
ctx.fill();
// ── Uplink beam cone (ground → sat) — offset, different color ──
const beamGradUp = ctx.createLinearGradient(to.x, to.y, from.x, from.y);
beamGradUp.addColorStop(0, `rgba(52,211,153,${0.14 * strength})`);
beamGradUp.addColorStop(0.5, `rgba(52,211,153,${0.06 * strength})`);
beamGradUp.addColorStop(1, `rgba(52,211,153,${0.02 * strength})`);
const offX = nx * 20; // lateral offset so beams don't overlap
const offY = ny * 20;
ctx.beginPath();
ctx.moveTo(from.x - nx * spreadTop + offX, from.y - ny * spreadTop + offY);
ctx.lineTo(to.x - nx * spreadBot * 0.6 + offX, to.y - ny * spreadBot * 0.6 + offY);
ctx.lineTo(to.x + nx * spreadBot * 0.1 + offX, to.y + ny * spreadBot * 0.1 + offY);
ctx.lineTo(from.x + nx * spreadTop * 0.1 + offX, from.y + ny * spreadTop * 0.1 + offY);
ctx.closePath();
ctx.fillStyle = beamGradUp;
ctx.fill();
// ── Downlink packets (sat → ground) — cyan ──
downPackets.forEach(p => {
p.progress += p.speed;
if (p.progress > 1) p.progress -= 1; // wrap around, never accumulate
const px = lerp(from.x, to.x, p.progress);
const py = lerp(from.y, to.y, p.progress);
const sz = 2.5 + Math.sin(p.progress * Math.PI) * 3;
const alpha = Math.sin(p.progress * Math.PI) * 0.8 * strength;
ctx.beginPath();
ctx.arc(px, py, sz, 0, Math.PI * 2);
ctx.fillStyle = `rgba(79,195,247,${alpha})`;
ctx.fill();
ctx.beginPath();
ctx.arc(px, py, sz + 4, 0, Math.PI * 2);
ctx.fillStyle = `rgba(79,195,247,${alpha * 0.25})`;
ctx.fill();
});
// ── Uplink packets (ground → sat) — green, offset path ──
upPackets.forEach(p => {
p.progress += p.speed;
if (p.progress > 1) p.progress -= 1;
const px = lerp(to.x, from.x, p.progress) + offX;
const py = lerp(to.y, from.y, p.progress) + offY;
const sz = 2 + Math.sin(p.progress * Math.PI) * 2.5;
const alpha = Math.sin(p.progress * Math.PI) * 0.7 * strength;
ctx.beginPath();
ctx.arc(px, py, sz, 0, Math.PI * 2);
ctx.fillStyle = `rgba(52,211,153,${alpha})`;
ctx.fill();
ctx.beginPath();
ctx.arc(px, py, sz + 3, 0, Math.PI * 2);
ctx.fillStyle = `rgba(52,211,153,${alpha * 0.25})`;
ctx.fill();
});
// ── Beam labels ──
const midX = lerp(from.x, to.x, 0.12);
const midY = lerp(from.y, to.y, 0.12);
ctx.font = '10px system-ui';
ctx.fillStyle = 'rgba(79,195,247,0.7)';
ctx.fillText('▼ Downlink', midX + 12, midY);
ctx.fillStyle = 'rgba(52,211,153,0.7)';
ctx.fillText('▲ Uplink', midX + offX - 50, midY + offY);
// ── Impairment markers along the beam ──
const impairments = [
{ id: 'tFSL', pos: 0.25, color: '#ff6b6b', label: 'FSPL 200 dB', symbol: '📉' },
{ id: 'tIono', pos: 0.55, color: '#c084fc', label: 'Iono scintillation', symbol: '🔮' },
{ id: 'tAtm', pos: 0.72, color: '#ffd93d', label: 'Gas absorption', symbol: '🌫️' },
{ id: 'tRain', pos: 0.82, color: '#6bcbff', label: 'Rain fade', symbol: '🌧️' },
{ id: 'tNoise', pos: 0.92, color: '#fb923c', label: 'N = kTB', symbol: '🌡️' },
{ id: 'tPoint', pos: 0.96, color: '#34d399', label: 'Pointing err.', symbol: '🎯' },
];
impairments.forEach(imp => {
if (!isOn(imp.id)) return;
const ix = lerp(from.x, to.x, imp.pos);
const iy = lerp(from.y, to.y, imp.pos);
// Pulse ring
const pulse = Math.sin(t * 2 + imp.pos * 10) * 0.3 + 0.7;
ctx.beginPath();
ctx.arc(ix, iy, 14 * pulse, 0, Math.PI * 2);
ctx.strokeStyle = imp.color;
ctx.lineWidth = 1.5;
ctx.globalAlpha = 0.6;
ctx.stroke();
ctx.globalAlpha = 1;
// Cross mark
const cs = 5;
ctx.beginPath();
ctx.moveTo(ix - cs, iy - cs); ctx.lineTo(ix + cs, iy + cs);
ctx.moveTo(ix + cs, iy - cs); ctx.lineTo(ix - cs, iy + cs);
ctx.strokeStyle = imp.color;
ctx.lineWidth = 2;
ctx.stroke();
// Label
ctx.fillStyle = imp.color;
ctx.font = '10px system-ui';
const labelX = ix + (imp.pos < 0.5 ? 20 : -ctx.measureText(imp.label).width - 20);
ctx.fillText(imp.label, labelX, iy + 4);
});
return strength;
}
function drawSignalBar(strength) {
const barX = 18;
const barY = H * 0.15;
const barW = 12;
const barH = H * 0.45;
// Background
ctx.fillStyle = 'rgba(30,58,95,0.5)';
ctx.strokeStyle = 'rgba(79,195,247,0.3)';
ctx.lineWidth = 1;
roundRect(ctx, barX, barY, barW, barH, 6);
ctx.fill();
ctx.stroke();
// Fill
const fillH = barH * strength;
const fillGrad = ctx.createLinearGradient(0, barY + barH - fillH, 0, barY + barH);
if (strength > 0.6) {
fillGrad.addColorStop(0, '#34d399');
fillGrad.addColorStop(1, '#059669');
} else if (strength > 0.3) {
fillGrad.addColorStop(0, '#fbbf24');
fillGrad.addColorStop(1, '#d97706');
} else {
fillGrad.addColorStop(0, '#f87171');
fillGrad.addColorStop(1, '#dc2626');
}
roundRect(ctx, barX + 1, barY + barH - fillH, barW - 2, fillH, 5);
ctx.fillStyle = fillGrad;
ctx.fill();
// Label
ctx.save();
ctx.translate(barX + barW + 6, barY + barH / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillStyle = '#64748b';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Signal Strength', 0, 0);
ctx.restore();
// Percentage
ctx.fillStyle = '#e2e8f0';
ctx.font = 'bold 12px system-ui';
ctx.textAlign = 'center';
ctx.fillText(Math.round(strength * 100) + '%', barX + barW/2, barY - 8);
ctx.textAlign = 'start';
}
function drawRain(earthY) {
if (!isOn('tRain')) return;
const rainZoneTop = earthY - 140;
const rainZoneBot = earthY - 10;
// Cloud
const cloudAlpha = 0.25 + Math.sin(t * 0.3) * 0.08;
ctx.fillStyle = `rgba(148,163,184,${cloudAlpha})`;
drawCloud(W * 0.42, rainZoneTop - 5, 60, 25);
drawCloud(W * 0.55, rainZoneTop + 10, 45, 20);
// Drops
drops.forEach(d => {
const dx = d.x * W * 0.5 + W * 0.25;
d.y += d.speed / H;
if (d.y > 1) { d.y = 0; d.x = Math.random(); }
const dy = rainZoneTop + d.y * (rainZoneBot - rainZoneTop);
ctx.beginPath();
ctx.moveTo(dx, dy);
ctx.lineTo(dx - 0.5, dy + d.len);
ctx.strokeStyle = `rgba(107,203,255,${d.opacity})`;
ctx.lineWidth = 1;
ctx.stroke();
});
}
function drawCloud(cx, cy, w, h) {
ctx.beginPath();
ctx.ellipse(cx, cy, w, h, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx - w * 0.45, cy + 5, w * 0.55, h * 0.7, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx + w * 0.4, cy + 3, w * 0.5, h * 0.65, 0, 0, Math.PI * 2);
ctx.fill();
}
function drawNoise(gndPos) {
if (!isOn('tNoise')) return;
for (let i = 0; i < 25; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = 15 + Math.random() * 30;
const nx = gndPos.x + Math.cos(angle) * dist;
const ny = gndPos.y + Math.sin(angle) * dist;
ctx.beginPath();
ctx.arc(nx, ny, 1 + Math.random() * 1.5, 0, Math.PI * 2);
ctx.fillStyle = `rgba(251,146,60,${0.2 + Math.random() * 0.4})`;
ctx.fill();
}
}
function drawPointingError(satPos, gndPos) {
if (!isOn('tPoint')) return;
const offset = Math.sin(t * 1.2) * 12;
ctx.setLineDash([4, 6]);
ctx.beginPath();
ctx.moveTo(gndPos.x, gndPos.y);
ctx.lineTo(gndPos.x + offset * 3, gndPos.y - 60);
ctx.strokeStyle = 'rgba(52,211,153,0.35)';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.setLineDash([]);
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
// ── Main loop ──
function draw() {
t += 0.016;
ctx.clearRect(0, 0, W, H);
// Background gradient (space)
const bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, '#020617');
bg.addColorStop(0.6, '#0a1628');
bg.addColorStop(1, '#0d1b2a');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
// Stars
stars.forEach(s => {
const alpha = 0.3 + Math.sin(t * s.speed + s.twinkle) * 0.35 + 0.35;
ctx.beginPath();
ctx.arc(s.x * W, s.y * H, s.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,255,${alpha})`;
ctx.fill();
});
// Earth
const earthY = drawEarth();
// Ground station
const gndPos = drawGroundStation(earthY);
// Satellite
const satPos = drawSatellite();
// Rain (behind beam)
drawRain(earthY);
// Signal beam
const strength = drawSignalBeam(satPos, gndPos);
// Noise sparkles
drawNoise(gndPos);
// Pointing error
drawPointingError(satPos, gndPos);
// Signal bar
drawSignalBar(strength);
// Title
ctx.fillStyle = '#e2e8f0';
ctx.font = 'bold 16px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Satellite Communication Link — Signal Impairments', W/2, 30);
ctx.textAlign = 'start';
requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>
"""
def render():
"""Render the satellite link animation page."""
st.markdown("## 🛰️ Satellite Link — Interactive Animation")
st.markdown(
"Visualise how a signal travels from a **GEO satellite** to a "
"**ground station** and discover the impairments that degrade it. "
"Toggle each effect on/off to see the impact on signal strength."
)
st.divider()
components.html(_ANIMATION_HTML, height=680, scrolling=False)
# ── Pedagogical summary below the animation ──
st.divider()
st.markdown("### 📚 Understanding the Link Budget")
col1, col2 = st.columns(2)
with col1:
st.markdown("""
**Uplink & Downlink path:**
The signal travels ~36 000 km from a geostationary satellite to Earth.
Along the way it encounters multiple sources of degradation:
1. **Free-Space Path Loss** — the dominant factor, purely geometric (1/r²)
2. **Atmospheric gases** — O₂ and H₂O absorption (ITU-R P.676)
3. **Rain** — scattering & absorption, worst at Ka-band (ITU-R P.618)
4. **Ionosphere** — Faraday rotation, scintillation (ITU-R P.531)
""")
with col2:
st.markdown("""
**At the receiver:**
Even after the signal arrives, further degradation occurs:
5. **Thermal noise** — every component adds noise: $N = k \\cdot T_{sys} \\cdot B$
6. **Pointing loss** — antenna misalignment reduces gain
7. **Implementation losses** — ADC quantisation, filter roll-off, etc.
The **Shannon limit** $C = B \\log_2(1 + C/N)$ tells us the maximum
bit rate achievable given the remaining signal-to-noise ratio.
""")
with st.expander("🔗 Key ITU-R Recommendations"):
st.markdown("""
| Recommendation | Topic |
|:---:|:---|
| **P.618** | Rain attenuation & propagation effects for satellite links |
| **P.676** | Gaseous attenuation on terrestrial and slant paths |
| **P.531** | Ionospheric effects on radiowave propagation |
| **P.837** | Rainfall rate statistics for prediction models |
| **P.839** | Rain height model for prediction methods |
| **S.1428** | Reference satellite link for system design |
""")