Files
Python-Shannon/views/orbits_animation.py
Poidevin, Antoine (ITOP CM) - AF ac6c0e1bdf
All checks were successful
Build & Deploy Shannon / 🏗️ Build & Deploy Shannon (push) Successful in 1m3s
refactor: remove emojis from titles and buttons for a cleaner UI
2026-02-20 10:50:04 +01:00

586 lines
19 KiB
Python
Raw 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.
"""
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 |
""")