All checks were successful
Build & Deploy Shannon / 🏗️ Build & Deploy Shannon (push) Successful in 1m3s
586 lines
19 KiB
Python
586 lines
19 KiB
Python
"""
|
||
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 |
|
||
""")
|