Files
Python-Shannon/views/satellite_animation.py
Poidevin, Antoine (ITOP CM) - AF 6a4ccc3376
All checks were successful
Build & Deploy Shannon / 🏗️ Build & Deploy Shannon (push) Successful in 3m1s
feat: add interactive exploration of Shannon's capacity formula with Plotly graphs
- 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.
2026-02-20 10:33:09 +01:00

763 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 |
""")