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

585
views/orbits_animation.py Normal file
View File

@@ -0,0 +1,585 @@
"""
GEO / MEO / LEO Orbits — Interactive Animation
================================================
Animated comparison of satellite orbit types with key trade-offs.
"""
import streamlit as st
import streamlit.components.v1 as components
_ORBITS_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; }
/* ── Info panel ── */
#info {
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: 18px 20px; width: 280px;
backdrop-filter: blur(12px); box-shadow: 0 8px 32px rgba(0,0,0,0.45);
color: #e2e8f0; font-size: 12px; line-height: 1.7;
transition: all 0.3s;
}
#info h3 { color: #4FC3F7; font-size: 14px; margin-bottom: 10px; text-align: center; }
#info .orbit-name { font-weight: 700; font-size: 13px; }
#info .stat { display: flex; justify-content: space-between; padding: 3px 0; }
#info .stat .label { color: #94a3b8; }
#info .stat .value { color: #e2e8f0; font-weight: 600; }
#info hr { border: none; border-top: 1px solid rgba(79,195,247,0.15); margin: 8px 0; }
/* ── Speed slider ── */
#speedControl {
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 18px; width: 220px;
backdrop-filter: blur(10px); color: #94a3b8; font-size: 12px;
}
#speedControl label { display: block; margin-bottom: 6px; color: #4FC3F7; font-weight: 600; }
#speedControl input[type=range] { width: 100%; accent-color: #4FC3F7; }
/* ── Legend ── */
#orbitLegend {
position: absolute; bottom: 14px; right: 18px;
background: rgba(13,27,42,0.88); border: 1px solid rgba(79,195,247,0.2);
border-radius: 12px; padding: 14px 18px; width: 280px;
backdrop-filter: blur(10px); color: #e2e8f0; font-size: 12px; line-height: 1.7;
}
#orbitLegend h4 { color: #4FC3F7; margin-bottom: 8px; font-size: 13px; }
.legend-item {
display: flex; align-items: center; gap: 10px; padding: 4px 0; cursor: pointer;
border-radius: 6px; padding: 4px 8px; transition: background 0.2s;
}
.legend-item:hover { background: rgba(79,195,247,0.08); }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.legend-label { flex: 1; }
.legend-detail { color: #64748b; font-size: 11px; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="info">
<h3>🛰️ Orbit Comparison</h3>
<div id="infoContent">
<p style="color:#94a3b8; text-align:center;">
Hover over a satellite to see its details.
</p>
</div>
</div>
<div id="speedControl">
<label>⏱️ Animation Speed</label>
<input type="range" id="speedSlider" min="0.1" max="5" step="0.1" value="1">
<div style="display:flex; justify-content:space-between; margin-top:4px;">
<span>Slow</span><span>Fast</span>
</div>
</div>
<div id="orbitLegend">
<h4>📐 Orbit Types</h4>
<div class="legend-item" data-orbit="leo">
<span class="legend-dot" style="background:#34d399"></span>
<span class="legend-label">LEO</span>
<span class="legend-detail">160 2 000 km</span>
</div>
<div class="legend-item" data-orbit="meo">
<span class="legend-dot" style="background:#fbbf24"></span>
<span class="legend-label">MEO</span>
<span class="legend-detail">2 000 35 786 km</span>
</div>
<div class="legend-item" data-orbit="geo">
<span class="legend-dot" style="background:#f87171"></span>
<span class="legend-label">GEO</span>
<span class="legend-detail">35 786 km (fixed)</span>
</div>
</div>
<script>
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();
// ── Stars ──
const stars = Array.from({length: 180}, () => ({
x: Math.random(), y: Math.random(),
r: Math.random() * 1.2 + 0.2,
tw: Math.random() * Math.PI * 2,
sp: 0.3 + Math.random() * 1.5,
}));
// ── Orbit definitions ──
// Radii proportional: Earth radius = 6371 km
// Visual scale: earthR in pixels, then orbits are proportional
const earthRealKm = 6371;
const orbits = [
{
name: 'LEO', fullName: 'Low Earth Orbit',
color: '#34d399', colorFade: 'rgba(52,211,153,',
altitudeKm: 550, // typical Starlink
periodMin: 95, // ~95 min
numSats: 8,
latencyMs: '4 20',
coverage: 'Small footprint (~1000 km)',
examples: 'Starlink, OneWeb, Iridium',
fsplDb: '~155 dB (Ku)',
pros: 'Low latency, lower FSPL',
cons: 'Many sats needed, handover required',
speedFactor: 6.0, // relative orbital speed (fastest)
},
{
name: 'MEO', fullName: 'Medium Earth Orbit',
color: '#fbbf24', colorFade: 'rgba(251,191,36,',
altitudeKm: 20200, // GPS
periodMin: 720, // 12h
numSats: 5,
latencyMs: '40 80',
coverage: 'Mid footprint (~12 000 km)',
examples: 'GPS, Galileo, O3b/SES',
fsplDb: '~186 dB (Ku)',
pros: 'Good latency/coverage balance',
cons: 'Moderate constellation size',
speedFactor: 1.5,
},
{
name: 'GEO', fullName: 'Geostationary Orbit',
color: '#f87171', colorFade: 'rgba(248,113,113,',
altitudeKm: 35786,
periodMin: 1436, // 23h56
numSats: 3,
latencyMs: '240 280',
coverage: 'Huge footprint (~1/3 Earth)',
examples: 'Intelsat, SES, Eutelsat',
fsplDb: '~205 dB (Ku)',
pros: 'Fixed position, 3 sats = global',
cons: 'High latency, high FSPL',
speedFactor: 0.3,
},
];
// ── Satellite objects ──
let satellites = [];
function initSats() {
satellites = [];
orbits.forEach(o => {
for (let i = 0; i < o.numSats; i++) {
satellites.push({
orbit: o,
angle: (Math.PI * 2 * i) / o.numSats + Math.random() * 0.3,
hovered: false,
});
}
});
}
initSats();
let t = 0;
let hoveredOrbit = null;
let mouseX = -1, mouseY = -1;
canvas.addEventListener('mousemove', (e) => {
mouseX = e.offsetX;
mouseY = e.offsetY;
});
canvas.addEventListener('mouseleave', () => {
mouseX = mouseY = -1;
hoveredOrbit = null;
document.getElementById('infoContent').innerHTML =
'<p style="color:#94a3b8; text-align:center;">Hover over a satellite to see its details.</p>';
});
// Legend hover
document.querySelectorAll('.legend-item').forEach(el => {
el.addEventListener('mouseenter', () => {
const name = el.dataset.orbit.toUpperCase();
const o = orbits.find(o => o.name === name);
if (o) showInfo(o);
});
el.addEventListener('mouseleave', () => {
hoveredOrbit = null;
document.getElementById('infoContent').innerHTML =
'<p style="color:#94a3b8; text-align:center;">Hover over a satellite to see its details.</p>';
});
});
function showInfo(o) {
hoveredOrbit = o.name;
document.getElementById('infoContent').innerHTML = `
<div class="orbit-name" style="color:${o.color}">${o.name} — ${o.fullName}</div>
<hr>
<div class="stat"><span class="label">Altitude</span><span class="value">${o.altitudeKm.toLocaleString()} km</span></div>
<div class="stat"><span class="label">Period</span><span class="value">${o.periodMin >= 60 ? (o.periodMin/60).toFixed(1)+'h' : o.periodMin+'min'}</span></div>
<div class="stat"><span class="label">Latency (RTT)</span><span class="value">${o.latencyMs} ms</span></div>
<div class="stat"><span class="label">FSPL</span><span class="value">${o.fsplDb}</span></div>
<div class="stat"><span class="label">Coverage</span><span class="value">${o.coverage}</span></div>
<hr>
<div class="stat"><span class="label">✅ Pros</span><span class="value" style="text-align:right; max-width:160px">${o.pros}</span></div>
<div class="stat"><span class="label">⚠️ Cons</span><span class="value" style="text-align:right; max-width:160px">${o.cons}</span></div>
<hr>
<div class="stat"><span class="label">Examples</span><span class="value" style="color:#4FC3F7">${o.examples}</span></div>
`;
}
function getOrbitRadius(altKm) {
const earthR = Math.min(W, H) * 0.12;
const maxAlt = 42000;
const maxOrbitR = Math.min(W, H) * 0.44;
return earthR + (altKm / maxAlt) * (maxOrbitR - earthR);
}
function drawBackground() {
const bg = ctx.createRadialGradient(W/2, H/2, 0, W/2, H/2, W * 0.7);
bg.addColorStop(0, '#0a1628');
bg.addColorStop(1, '#020617');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
stars.forEach(s => {
const alpha = 0.3 + Math.sin(t * s.sp + s.tw) * 0.3 + 0.3;
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();
});
}
function drawEarth() {
const cx = W / 2;
const cy = H / 2;
const r = Math.min(W, H) * 0.12;
// Glow
const glow = ctx.createRadialGradient(cx, cy, r * 0.8, cx, cy, r * 1.6);
glow.addColorStop(0, 'rgba(56,189,248,0.1)');
glow.addColorStop(1, 'transparent');
ctx.fillStyle = glow;
ctx.fillRect(0, 0, W, H);
// Body
const grad = ctx.createRadialGradient(cx - r*0.3, cy - r*0.3, r*0.1, cx, cy, r);
grad.addColorStop(0, '#2dd4bf');
grad.addColorStop(0.3, '#0f766e');
grad.addColorStop(0.6, '#0e4f72');
grad.addColorStop(1, '#0c2d48');
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
// Atmosphere rim
ctx.beginPath();
ctx.arc(cx, cy, r + 3, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(56,189,248,0.3)';
ctx.lineWidth = 4;
ctx.stroke();
// Label
ctx.fillStyle = '#e2e8f0';
ctx.font = 'bold 13px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Earth', cx, cy + 4);
ctx.font = '10px system-ui';
ctx.fillStyle = '#64748b';
ctx.fillText('R = 6 371 km', cx, cy + 18);
ctx.textAlign = 'start';
return { cx, cy, r };
}
function drawOrbits(earth) {
orbits.forEach(o => {
const r = getOrbitRadius(o.altitudeKm);
const isHighlight = hoveredOrbit === o.name;
// Orbit path
ctx.beginPath();
ctx.arc(earth.cx, earth.cy, r, 0, Math.PI * 2);
ctx.strokeStyle = isHighlight
? o.color
: o.colorFade + '0.18)';
ctx.lineWidth = isHighlight ? 2.5 : 1;
ctx.setLineDash(o.name === 'GEO' ? [] : [6, 4]);
ctx.stroke();
ctx.setLineDash([]);
// Coverage cone (for highlighted orbit)
if (isHighlight) {
const coneAngle = o.name === 'GEO' ? 0.28 : o.name === 'MEO' ? 0.18 : 0.08;
ctx.beginPath();
ctx.moveTo(earth.cx, earth.cy);
// Draw cone from earth center to orbit
const sampleAngle = satellites.find(s => s.orbit.name === o.name)?.angle || 0;
const sx = earth.cx + Math.cos(sampleAngle) * r;
const sy = earth.cy + Math.sin(sampleAngle) * r;
ctx.moveTo(sx, sy);
ctx.lineTo(
sx + Math.cos(sampleAngle + Math.PI + coneAngle) * r * 0.7,
sy + Math.sin(sampleAngle + Math.PI + coneAngle) * r * 0.7,
);
ctx.lineTo(
sx + Math.cos(sampleAngle + Math.PI - coneAngle) * r * 0.7,
sy + Math.sin(sampleAngle + Math.PI - coneAngle) * r * 0.7,
);
ctx.closePath();
ctx.fillStyle = o.colorFade + '0.06)';
ctx.fill();
}
// Altitude label on orbit ring
const labelAngle = -Math.PI * 0.25;
const lx = earth.cx + Math.cos(labelAngle) * (r + 14);
const ly = earth.cy + Math.sin(labelAngle) * (r + 14);
ctx.fillStyle = isHighlight ? o.color : o.colorFade + '0.5)';
ctx.font = isHighlight ? 'bold 11px system-ui' : '10px system-ui';
ctx.textAlign = 'center';
ctx.fillText(
`${o.name} · ${o.altitudeKm >= 10000 ? (o.altitudeKm/1000).toFixed(0)+'k' : o.altitudeKm.toLocaleString()} km`,
lx, ly
);
ctx.textAlign = 'start';
});
}
function drawSatellites(earth) {
const speed = parseFloat(document.getElementById('speedSlider').value);
satellites.forEach(sat => {
const r = getOrbitRadius(sat.orbit.altitudeKm);
sat.angle += sat.orbit.speedFactor * speed * 0.001;
const sx = earth.cx + Math.cos(sat.angle) * r;
const sy = earth.cy + Math.sin(sat.angle) * r;
// Hit test
const dist = Math.sqrt((mouseX - sx)**2 + (mouseY - sy)**2);
sat.hovered = dist < 18;
if (sat.hovered) showInfo(sat.orbit);
const isHighlight = hoveredOrbit === sat.orbit.name;
const satSize = isHighlight ? 7 : 5;
// Satellite glow
if (isHighlight) {
ctx.beginPath();
ctx.arc(sx, sy, satSize + 8, 0, Math.PI * 2);
ctx.fillStyle = sat.orbit.colorFade + '0.15)';
ctx.fill();
}
// Satellite body
ctx.beginPath();
ctx.arc(sx, sy, satSize, 0, Math.PI * 2);
ctx.fillStyle = isHighlight ? sat.orbit.color : sat.orbit.colorFade + '0.7)';
ctx.fill();
// Solar panel lines
const panelLen = isHighlight ? 10 : 6;
const pAngle = sat.angle + Math.PI / 2;
ctx.beginPath();
ctx.moveTo(sx - Math.cos(pAngle) * panelLen, sy - Math.sin(pAngle) * panelLen);
ctx.lineTo(sx + Math.cos(pAngle) * panelLen, sy + Math.sin(pAngle) * panelLen);
ctx.strokeStyle = isHighlight ? sat.orbit.color : sat.orbit.colorFade + '0.4)';
ctx.lineWidth = isHighlight ? 2.5 : 1.5;
ctx.stroke();
// Signal line to Earth (when highlighted)
if (isHighlight) {
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(earth.cx, earth.cy);
ctx.strokeStyle = sat.orbit.colorFade + '0.12)';
ctx.lineWidth = 1;
ctx.setLineDash([3, 5]);
ctx.stroke();
ctx.setLineDash([]);
}
});
}
function drawLatencyComparison(earth) {
const barX = 46;
const barY = H * 0.08;
const barH = 18;
const maxMs = 300;
const gap = 6;
ctx.fillStyle = '#94a3b8';
ctx.font = 'bold 11px system-ui';
ctx.fillText('Round-Trip Latency', barX, barY - 10);
orbits.forEach((o, i) => {
const y = barY + i * (barH + gap);
const latAvg = o.name === 'LEO' ? 12 : o.name === 'MEO' ? 60 : 260;
const barW = (latAvg / maxMs) * 160;
// Bar bg
ctx.fillStyle = 'rgba(30,58,95,0.4)';
roundRect(ctx, barX, y, 160, barH, 4);
ctx.fill();
// Bar fill
const fillGrad = ctx.createLinearGradient(barX, 0, barX + barW, 0);
fillGrad.addColorStop(0, o.colorFade + '0.8)');
fillGrad.addColorStop(1, o.colorFade + '0.4)');
roundRect(ctx, barX, y, barW, barH, 4);
ctx.fillStyle = fillGrad;
ctx.fill();
// Label
ctx.fillStyle = '#e2e8f0';
ctx.font = '10px system-ui';
ctx.fillText(`${o.name} ${o.latencyMs} ms`, barX + 6, y + 13);
});
}
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);
drawBackground();
const earth = drawEarth();
drawOrbits(earth);
drawSatellites(earth);
drawLatencyComparison(earth);
// Title
ctx.fillStyle = '#e2e8f0';
ctx.font = 'bold 16px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Satellite Orbits — GEO vs MEO vs LEO', W/2, 30);
ctx.textAlign = 'start';
requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>
"""
def render():
"""Render the GEO/MEO/LEO orbit comparison animation."""
st.markdown("## 🌍 Satellite Orbits — GEO vs MEO vs LEO")
st.markdown(
"Compare the three main satellite orbit types. "
"Hover over satellites or legend items to explore their characteristics."
)
st.divider()
components.html(_ORBITS_HTML, height=680, scrolling=False)
# ── Educational content below ──
st.divider()
st.markdown("### 📊 Orbit Comparison at a Glance")
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("""
#### 🟢 LEO — Low Earth Orbit
**160 2 000 km**
- ⚡ Latency: **4 20 ms** (RTT)
- 📉 FSPL: ~155 dB (Ku-band)
- 🔄 Period: ~90 120 min
- 📡 Small footprint → **large constellations** needed (hundreds to thousands)
- 🤝 Requires **handover** between satellites
- 🛰️ *Starlink (550 km), OneWeb (1 200 km), Iridium (780 km)*
""")
with col2:
st.markdown("""
#### 🟡 MEO — Medium Earth Orbit
**2 000 35 786 km**
- ⚡ Latency: **40 80 ms** (RTT)
- 📉 FSPL: ~186 dB (Ku-band)
- 🔄 Period: ~2 12 h
- 📡 Medium footprint → **medium constellations** (20 50 sats)
- 🌍 Good balance between coverage and latency
- 🛰️ *GPS (20 200 km), Galileo, O3b/SES (8 000 km)*
""")
with col3:
st.markdown("""
#### 🔴 GEO — Geostationary Orbit
**35 786 km (fixed)**
- ⚡ Latency: **240 280 ms** (RTT)
- 📉 FSPL: ~205 dB (Ku-band)
- 🔄 Period: 23 h 56 min (= 1 sidereal day)
- 📡 Huge footprint → **3 sats = global** coverage
- 📌 **Fixed position** in the sky — no tracking needed
- 🛰️ *Intelsat, SES, Eutelsat, ViaSat*
""")
with st.expander("🔬 Key Trade-offs in Detail"):
st.markdown(r"""
| Parameter | LEO | MEO | GEO |
|:---|:---:|:---:|:---:|
| **Altitude** | 160 2 000 km | 2 000 35 786 km | 35 786 km |
| **Round-trip latency** | 4 20 ms | 40 80 ms | 240 280 ms |
| **Free-Space Path Loss** | ~155 dB | ~186 dB | ~205 dB |
| **Orbital period** | 90 120 min | 2 12 h | 23h 56m |
| **Coverage per sat** | ~1 000 km | ~12 000 km | ~15 000 km |
| **Constellation size** | Hundreds thousands | 20 50 | 3 |
| **Antenna tracking** | Required (fast) | Required (slow) | Fixed dish |
| **Doppler shift** | High | Moderate | Negligible |
| **Launch cost/sat** | Lower | Medium | Higher |
| **Orbital lifetime** | 5 7 years | 10 15 years | 15+ years |
**FSPL formula:** $\text{FSPL (dB)} = 20 \log_{10}(d) + 20 \log_{10}(f) + 32.44$
Where $d$ is distance in km and $f$ is frequency in MHz.
**Why it matters for Shannon:**
The higher the orbit, the greater the FSPL, the lower the received C/N.
Since $C = B \log_2(1 + C/N)$, a higher orbit means lower achievable bit rate
for the same bandwidth and transmit power. LEO compensates with lower FSPL
but requires more satellites and complex handover.
""")
with st.expander("🛰️ Notable Constellations"):
st.markdown("""
| Constellation | Orbit | Altitude | # Satellites | Use Case |
|:---|:---:|:---:|:---:|:---|
| **Starlink** | LEO | 550 km | ~6 000+ | Broadband Internet |
| **OneWeb** | LEO | 1 200 km | ~650 | Enterprise connectivity |
| **Iridium NEXT** | LEO | 780 km | 66 + spares | Global voice/data |
| **O3b mPOWER** | MEO | 8 000 km | 11+ | Managed connectivity |
| **GPS** | MEO | 20 200 km | 31 | Navigation |
| **Galileo** | MEO | 23 222 km | 30 | Navigation |
| **Intelsat** | GEO | 35 786 km | 50+ | Video & enterprise |
| **SES** | GEO + MEO | Mixed | 70+ | Video & data |
| **Eutelsat** | GEO | 35 786 km | 35+ | Video broadcasting |
""")