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

0
views/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

130
views/contributions.py Normal file
View File

@@ -0,0 +1,130 @@
"""
Page 3: Community Contributions
Read, write, search and delete user contributions stored in SQLite.
"""
import streamlit as st
from core.database import write_contribution, search_contributions, delete_contribution
def render():
"""Render the Contributions page."""
st.markdown("# 💬 Community Contributions")
st.markdown(
"Share your observations about Shannon's theorem, satellite communications, "
"or suggest improvements. Contributions are stored locally and shared with all users."
)
tab_read, tab_write = st.tabs(["📖 Read Contributions", "✍️ Write Contribution"])
# ── Read Contributions ──
with tab_read:
st.markdown("### 🔍 Search Contributions")
db_choice = st.radio(
"Database",
["Theory", "Real World"],
horizontal=True,
key="db_read",
)
db_name = "Shannon_Theory.db" if db_choice == "Theory" else "Shannon_Real.db"
col_f1, col_f2 = st.columns(2)
with col_f1:
name_filter = st.text_input("Filter by Name", key="filter_name")
title_filter = st.text_input("Filter by Title", key="filter_title")
with col_f2:
kw_filter = st.text_input("Filter by Keywords", key="filter_kw")
content_filter = st.text_input("Filter by Content", key="filter_content")
if st.button("🔍 Search", type="primary", key="btn_search"):
results = search_contributions(
db_name,
name_filter=name_filter,
title_filter=title_filter,
keywords_filter=kw_filter,
content_filter=content_filter,
)
st.session_state["contrib_results"] = results
results = st.session_state.get("contrib_results", [])
if results:
st.success(f"Found {len(results)} contribution(s).")
for i, contrib in enumerate(results):
with st.expander(
f"#{contrib['num']}{contrib['title']} by {contrib['name']} ({contrib['date']})",
expanded=(i == 0),
):
st.markdown(contrib["text"])
if contrib["keywords"]:
st.caption(f"🏷️ Keywords: {contrib['keywords']}")
# Delete functionality
with st.popover("🗑️ Delete"):
st.warning("This action cannot be undone.")
del_password = st.text_input(
"Enter contribution password",
type="password",
key=f"del_pw_{contrib['num']}",
)
if st.button("Confirm Delete", key=f"del_btn_{contrib['num']}"):
if delete_contribution(db_name, contrib["num"], del_password):
st.success(f"✅ Contribution #{contrib['num']} deleted.")
# Refresh results
st.session_state["contrib_results"] = search_contributions(
db_name,
name_filter=name_filter,
title_filter=title_filter,
keywords_filter=kw_filter,
content_filter=content_filter,
)
st.rerun()
else:
st.error("❌ Incorrect password or contribution not found.")
elif "contrib_results" in st.session_state:
st.info("No contributions found matching your filters.")
# ── Write Contribution ──
with tab_write:
st.markdown("### ✍️ New Contribution")
db_choice_w = st.radio(
"Database",
["Theory", "Real World"],
horizontal=True,
key="db_write",
)
db_name_w = "Shannon_Theory.db" if db_choice_w == "Theory" else "Shannon_Real.db"
with st.form("contribution_form", clear_on_submit=True):
name = st.text_input("Your Name / Initials *", max_chars=100)
title = st.text_input("Title *", max_chars=200)
keywords = st.text_input("Keywords (comma-separated)", max_chars=200)
text = st.text_area("Your contribution *", height=200)
password = st.text_input(
"Password (optional — leave empty to allow anyone to delete)",
type="password",
)
submitted = st.form_submit_button("📤 Submit", type="primary")
if submitted:
if not name or not title or not text:
st.error("❌ Please fill in all required fields (marked with *).")
else:
new_id = write_contribution(db_name_w, name, title, keywords, text, password)
st.success(f"✅ Thank you! Your contribution has been stored with ID #{new_id}.")
st.balloons()
with st.expander("❓ Help"):
st.markdown(
"Write your contribution as a free text. Contributions should be:\n\n"
"- Candid observations about the technical subject\n"
"- References to relevant material (ideally as web links)\n"
"- Open discussion items about adjacent subjects\n"
"- Suggestions for improvement\n\n"
"You can retrieve and delete your contribution from the Read tab using your password."
)

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 |
""")

412
views/real_world.py Normal file
View File

@@ -0,0 +1,412 @@
"""
Page 2: Shannon and Friends in the Real World
Complete satellite link budget with interactive Plotly graphs.
"""
import streamlit as st
import numpy as np
import plotly.graph_objects as go
from math import log
from core.calculations import (
combine_cnr,
shannon_capacity,
compute_satellite_link,
compute_receiver,
compute_baseband,
fmt_br,
fmt_power,
fmt_gain,
fmt_pfd,
fmt_psd,
fmt_ploss,
)
from core.help_texts import REAL_WORLD_HELP
def _make_bw_sensitivity_real(
cnr_nyq, bandwidth, rolloff, overheads, penalties, cnr_imp, sat_cir, hpa_power
) -> go.Figure:
n = 40
bw = np.zeros(n)
br = np.zeros(n)
cnr = np.zeros(n)
cnr[0] = cnr_nyq + 10 * log(8, 10)
bw[0] = bandwidth / 8
cnr_rcv_0 = combine_cnr(cnr[0], cnr_imp, sat_cir)
br[0] = shannon_capacity(bw[0] / (1 + rolloff / 100), cnr_rcv_0, penalties) / (1 + overheads / 100)
for i in range(1, n):
bw[i] = bw[i - 1] * 2 ** (1 / 6)
cnr[i] = cnr[i - 1] - 10 * log(bw[i] / bw[i - 1], 10)
cnr_rcv_i = combine_cnr(cnr[i], cnr_imp, sat_cir)
br[i] = shannon_capacity(bw[i] / (1 + rolloff / 100), cnr_rcv_i, penalties) / (1 + overheads / 100)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=bw, y=br, mode="lines",
name="Higher Layers BR",
line=dict(color="#4FC3F7", width=3),
))
# Reference point
cnr_rcv_ref = combine_cnr(cnr_nyq, cnr_imp, sat_cir)
br_ref = shannon_capacity(bandwidth / (1 + rolloff / 100), cnr_rcv_ref, penalties) / (1 + overheads / 100)
fig.add_trace(go.Scatter(
x=[bandwidth], y=[br_ref], mode="markers+text",
name=f"Ref: {bandwidth:.0f} MHz, {br_ref:.1f} Mbps",
marker=dict(size=12, color="#FF7043", symbol="diamond"),
text=[f"{br_ref:.1f} Mbps"],
textposition="top center",
))
fig.update_layout(
title=f"Higher Layers Bit Rate at Constant HPA Power: {hpa_power:.1f} W",
xaxis_title="Occupied Bandwidth [MHz]",
yaxis_title="Bit Rate [Mbps]",
template="plotly_dark",
height=500,
)
return fig
def _make_power_sensitivity_real(
cnr_nyq, bw_nyq, hpa_power, overheads, penalties, cnr_imp, sat_cir, bandwidth
) -> go.Figure:
n = 40
power = np.zeros(n)
br = np.zeros(n)
cnr = np.zeros(n)
power[0] = hpa_power / 8
cnr[0] = cnr_nyq - 10 * log(8, 10)
cnr_rcv_i = combine_cnr(cnr[0], cnr_imp, sat_cir)
br[0] = shannon_capacity(bw_nyq, cnr_rcv_i, penalties) / (1 + overheads / 100)
for i in range(1, n):
power[i] = power[i - 1] * 2 ** (1 / 6)
cnr[i] = cnr[i - 1] + 10 * log(2 ** (1 / 6), 10)
cnr_rcv_i = combine_cnr(cnr[i], cnr_imp, sat_cir)
br[i] = shannon_capacity(bw_nyq, cnr_rcv_i, penalties) / (1 + overheads / 100)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=power, y=br, mode="lines",
name="Higher Layers BR",
line=dict(color="#81C784", width=3),
))
cnr_rcv_ref = combine_cnr(cnr_nyq, cnr_imp, sat_cir)
br_ref = shannon_capacity(bw_nyq, cnr_rcv_ref, penalties) / (1 + overheads / 100)
fig.add_trace(go.Scatter(
x=[hpa_power], y=[br_ref], mode="markers+text",
name=f"Ref: {hpa_power:.0f} W, {br_ref:.1f} Mbps",
marker=dict(size=12, color="#FF7043", symbol="diamond"),
text=[f"{br_ref:.1f} Mbps"],
textposition="top center",
))
fig.update_layout(
title=f"Higher Layers Bit Rate at Constant BW: {bandwidth:.1f} MHz",
xaxis_title="HPA Output Power [W]",
yaxis_title="Bit Rate [Mbps]",
template="plotly_dark",
height=500,
)
return fig
def _make_br_factor_map_real(
cnr_nyq, bw_nyq, rolloff, overheads, penalties, cnr_imp, sat_cir,
hpa_power, bandwidth, br_rcv_higher,
) -> go.Figure:
n = 41
bw_mul = np.zeros((n, n))
p_mul = np.zeros((n, n))
br_mul = np.zeros((n, n))
for i in range(n):
for j in range(n):
bw_mul[i, j] = (i + 1) / 8
p_mul[i, j] = (j + 1) / 8
cnr_ij = cnr_nyq + 10 * log(p_mul[i, j] / bw_mul[i, j], 10)
cnr_rcv_ij = combine_cnr(cnr_ij, cnr_imp, sat_cir)
bw_ij = bw_nyq * bw_mul[i, j]
br_ij = shannon_capacity(bw_ij / (1 + rolloff / 100), cnr_rcv_ij, penalties) / (1 + overheads / 100)
br_mul[i, j] = br_ij / br_rcv_higher if br_rcv_higher > 0 else 0
fig = go.Figure(data=go.Contour(
z=br_mul,
x=bw_mul[:, 0],
y=p_mul[0, :],
colorscale="Viridis",
contours=dict(showlabels=True, labelfont=dict(size=10, color="white")),
colorbar=dict(title="BR Factor"),
))
fig.update_layout(
title=f"BR Multiplying Factor<br><sub>Ref: {hpa_power:.0f} W, "
f"{bandwidth:.0f} MHz, {br_rcv_higher:.1f} Mbps</sub>",
xaxis_title="Bandwidth Factor",
yaxis_title="Power Factor",
template="plotly_dark",
height=550,
)
return fig
def render():
"""Render the Real World satellite link budget page."""
# ── Header ──
col_img, col_title = st.columns([1, 3])
with col_img:
st.image("Satellite.png", width=200)
with col_title:
st.markdown("# 🛰️ Shannon & Friends in the Real World")
st.markdown("From theory to satellite communication link budget.")
wiki_cols = st.columns(4)
wiki_cols[0].link_button("📖 Harry Nyquist", "https://en.wikipedia.org/wiki/Harry_Nyquist")
wiki_cols[1].link_button("📖 Richard Hamming", "https://en.wikipedia.org/wiki/Richard_Hamming")
wiki_cols[2].link_button("📖 Andrew Viterbi", "https://en.wikipedia.org/wiki/Andrew_Viterbi")
wiki_cols[3].link_button("📖 Claude Berrou", "https://en.wikipedia.org/wiki/Claude_Berrou")
st.divider()
# ══════════════════════════════════════════════════════════════════════════
# SECTION 1: Satellite Link
# ══════════════════════════════════════════════════════════════════════════
st.markdown("## 📡 Satellite Link")
col1, col2, col3 = st.columns(3)
with col1:
sat_alt = st.number_input("Satellite Altitude [km]", value=35786.0, step=100.0,
help=REAL_WORLD_HELP["sat_alt"])
sat_lat = st.number_input("Satellite Latitude [°]", value=0.0, step=0.1,
help=REAL_WORLD_HELP["sat_latlon"])
sat_lon = st.number_input("Satellite Longitude [°]", value=19.2, step=0.1,
help=REAL_WORLD_HELP["sat_latlon"])
with col2:
gs_lat = st.number_input("Ground Station Latitude [°]", value=49.7, step=0.1,
help=REAL_WORLD_HELP["gs_latlon"])
gs_lon = st.number_input("Ground Station Longitude [°]", value=6.3, step=0.1,
help=REAL_WORLD_HELP["gs_latlon"])
freq = st.number_input("Frequency [GHz]", value=12.0, min_value=0.1, step=0.5,
help=REAL_WORLD_HELP["freq"])
with col3:
hpa_power = st.number_input("HPA Output Power [W]", value=120.0, min_value=0.1, step=10.0,
help=REAL_WORLD_HELP["hpa"])
sat_loss = st.number_input("Output Losses [dB]", value=2.0, min_value=0.0, step=0.5,
help=REAL_WORLD_HELP["losses"])
availability = st.number_input("Link Availability [%]", value=99.9,
min_value=90.0, max_value=99.999, step=0.1,
help=REAL_WORLD_HELP["availability"])
col4, col5 = st.columns(2)
with col4:
sat_cir_input = st.text_input("TX Impairments C/I [dB] (comma-sep)", value="25, 25",
help=REAL_WORLD_HELP["sat_cir"])
sat_beam = st.number_input("Beam Diameter [°]", value=3.0, min_value=0.1, step=0.1,
help=REAL_WORLD_HELP["sat_beam"])
with col5:
gain_offset = st.number_input("Offset from Peak [dB]", value=0.0, min_value=0.0, step=0.5,
help=REAL_WORLD_HELP["gain_offset"])
# Parse satellite C/I
try:
sat_cir_list = [float(v.strip()) for v in sat_cir_input.split(",")]
except ValueError:
st.error("❌ Invalid C/I values.")
return
# Compute satellite link
try:
with st.spinner("Computing atmospheric attenuation (ITU-R model)..."):
sat = compute_satellite_link(
freq, hpa_power, sat_loss, sat_cir_list, sat_beam, gain_offset,
sat_alt, sat_lat, sat_lon, gs_lat, gs_lon, availability,
)
except Exception as e:
st.error(f"❌ Satellite link computation error: {e}")
return
# Display satellite link results
st.markdown("#### 📊 Satellite Link Results")
r1, r2, r3 = st.columns(3)
r1.metric("Output Power", fmt_power(sat["sig_power"]), help=REAL_WORLD_HELP["output_power"])
r2.metric("Antenna Gain", fmt_gain(sat["sat_gain_linear"]), help=REAL_WORLD_HELP["sat_gain"])
r3.metric("EIRP", fmt_power(sat["eirp_linear"]), help=REAL_WORLD_HELP["eirp"])
r4, r5, r6 = st.columns(3)
r4.metric("Path Length", f"{sat['path_length']:.1f} km @ {sat['elevation']:.1f}°",
help=REAL_WORLD_HELP["path_length"])
r5.metric("Atmospheric Loss", f"{sat['atm_loss_db']:.1f} dB",
help=REAL_WORLD_HELP["atm_loss"])
r6.metric("Path Dispersion", fmt_ploss(sat["path_loss_linear"]),
help=REAL_WORLD_HELP["path_loss"])
st.metric("Power Flux Density", fmt_pfd(sat["pfd_linear"]), help=REAL_WORLD_HELP["pfd"])
if sat["elevation"] <= 0:
st.warning("⚠️ Satellite is below the horizon (negative elevation). Results may not be meaningful.")
st.divider()
# ══════════════════════════════════════════════════════════════════════════
# SECTION 2: Radio Front End
# ══════════════════════════════════════════════════════════════════════════
st.markdown("## 📻 Radio Front End")
col_r1, col_r2 = st.columns(2)
with col_r1:
cpe_ant_d = st.number_input("Receive Antenna Size [m]", value=0.6, min_value=0.1, step=0.1,
help=REAL_WORLD_HELP["cpe_ant"])
with col_r2:
cpe_t_clear = st.number_input("Noise Temperature [K]", value=120.0, min_value=10.0, step=10.0,
help=REAL_WORLD_HELP["cpe_temp"])
try:
rcv = compute_receiver(
sat["pfd_linear"], sat["atm_loss_db"], sat["wavelength"],
cpe_ant_d, cpe_t_clear,
)
except Exception as e:
st.error(f"❌ Receiver computation error: {e}")
return
st.markdown("#### 📊 Receiver Results")
rx1, rx2 = st.columns(2)
rx1.metric("Antenna Area · G/T", f"{rcv['cpe_ae']:.2f} m² · {rcv['cpe_g_t']:.1f} dB/K",
help=REAL_WORLD_HELP["cpe_gain"])
rx2.metric("RX Power (C)", fmt_power(rcv["rx_power"]), help=REAL_WORLD_HELP["rx_power"])
rx3, rx4 = st.columns(2)
rx3.metric("N₀", fmt_psd(rcv["n0"] * 1e6), help=REAL_WORLD_HELP["n0"])
rx4.metric("BR at ∞ BW", fmt_br(rcv["br_infinity"]), help=REAL_WORLD_HELP["br_inf"])
rx5, rx6 = st.columns(2)
rx5.metric("BR at SpEff=1", fmt_br(rcv["br_spe_1"]), help=REAL_WORLD_HELP["br_unit"])
rx6.metric("BR at SpEff=2", fmt_br(rcv["br_spe_double"]), help=REAL_WORLD_HELP["br_double"])
st.divider()
# ══════════════════════════════════════════════════════════════════════════
# SECTION 3: Baseband Unit
# ══════════════════════════════════════════════════════════════════════════
st.markdown("## 💻 Baseband Unit")
col_b1, col_b2, col_b3 = st.columns(3)
with col_b1:
bandwidth = st.number_input("Occupied Bandwidth [MHz]", value=36.0, min_value=0.1, step=1.0,
help=REAL_WORLD_HELP["bandwidth"], key="bw_real")
rolloff = st.number_input("Nyquist Rolloff [%]", value=5.0, min_value=0.0, max_value=100.0, step=1.0,
help=REAL_WORLD_HELP["rolloff"])
with col_b2:
cir_input = st.text_input("RX Impairments C/I [dB]", value="20",
help=REAL_WORLD_HELP["cir"])
penalties = st.number_input("Implementation Penalty [dB]", value=1.5, min_value=0.0, step=0.1,
help=REAL_WORLD_HELP["penalty"])
with col_b3:
overheads = st.number_input("Higher Layers Overhead [%]", value=5.0, min_value=0.0, step=1.0,
help=REAL_WORLD_HELP["overhead"])
try:
cnr_imp_list = [float(v.strip()) for v in cir_input.split(",")]
except ValueError:
st.error("❌ Invalid C/I values.")
return
try:
bb = compute_baseband(
rcv["c_n0_mhz"], rcv["br_infinity"], rcv["bw_spe_1"],
sat["sat_cir"], bandwidth, rolloff, overheads, cnr_imp_list, penalties,
)
except Exception as e:
st.error(f"❌ Baseband computation error: {e}")
return
st.markdown("#### 📊 Baseband Results")
b1, b2, b3 = st.columns(3)
b1.metric("SNR in Available BW", f"{bb['cnr_bw']:.1f} dB in {bandwidth:.1f} MHz",
help=REAL_WORLD_HELP["cnr_bw"])
b2.metric("SNR in Nyquist BW", f"{bb['cnr_nyq']:.1f} dB in {bb['bw_nyq']:.1f} MHz",
help=REAL_WORLD_HELP["cnr_nyq"])
b3.metric("SNR at Receiver", f"{bb['cnr_rcv']:.1f} dB",
help=REAL_WORLD_HELP["cnr_rcv"])
st.divider()
b4, b5 = st.columns(2)
with b4:
st.markdown("##### 🎯 Theoretical")
st.metric(
"Theoretical BR",
f"{fmt_br(bb['br_nyq'])} · {bb['br_nyq_norm']:.0%}",
help=REAL_WORLD_HELP["br_nyq"],
)
st.caption(f"Spectral Eff: {bb['spe_nyq']:.2f} bps/Hz · {bb['bits_per_symbol']:.2f} b/Symbol")
with b5:
st.markdown("##### 🏭 Practical")
st.metric(
"Physical Layer BR",
f"{fmt_br(bb['br_rcv'])} · {bb['br_rcv_norm']:.0%}",
help=REAL_WORLD_HELP["br_rcv"],
)
st.metric(
"Higher Layers BR",
f"{fmt_br(bb['br_rcv_higher'])} · {bb['br_rcv_h_norm']:.0%}",
delta=f"{bb['spe_higher']:.2f} bps/Hz",
help=REAL_WORLD_HELP["br_high"],
)
st.divider()
# ── Graphs ──
st.markdown("### 📈 Interactive Graphs")
tab_bw, tab_pow, tab_map = st.tabs([
"📶 BW Sensitivity",
"⚡ Power Sensitivity",
"🗺️ BR Factor Map",
])
cnr_imp = combine_cnr(*cnr_imp_list)
with tab_bw:
st.plotly_chart(
_make_bw_sensitivity_real(
bb["cnr_nyq"], bandwidth, rolloff, overheads, penalties,
cnr_imp, sat["sat_cir"], hpa_power,
),
width="stretch",
)
with tab_pow:
st.plotly_chart(
_make_power_sensitivity_real(
bb["cnr_nyq"], bb["bw_nyq"], hpa_power, overheads, penalties,
cnr_imp, sat["sat_cir"], bandwidth,
),
width="stretch",
)
with tab_map:
st.plotly_chart(
_make_br_factor_map_real(
bb["cnr_nyq"], bb["bw_nyq"], rolloff, overheads, penalties,
cnr_imp, sat["sat_cir"], hpa_power, bandwidth, bb["br_rcv_higher"],
),
width="stretch",
)
# ── Help ──
with st.expander("📘 Background Information"):
help_topic = st.selectbox(
"Choose a topic:",
options=["satellite", "advanced", "help"],
format_func=lambda x: {
"satellite": "🛰️ Link Budget Overview",
"advanced": "🔧 Advanced Notes",
"help": "❓ How to use",
}[x],
key="help_real",
)
st.markdown(REAL_WORLD_HELP[help_topic])

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 |
""")

917
views/satellite_types.py Normal file
View File

@@ -0,0 +1,917 @@
"""
Satellite Types — Mission Control Dashboard
=============================================
Each satellite category has its own unique animated scene:
- Navigation → GPS triangulation on a world map
- Communication → Data-flow network between ground stations
- Earth Observation → Top-down terrain scan with image strips
- Weather → Atmospheric cloud/rain/temperature visualisation
- Science → Deep-space telescope view (nebula zoom)
- Defense → Tactical radar sweep with blips
"""
import streamlit as st
import streamlit.components.v1 as components
_SATTYPES_HTML = r"""
<!DOCTYPE html>
<html lang="en">
<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;color:#e2e8f0}
/* ── Tab bar ── */
#tabs{
position:absolute;top:0;left:0;right:0;height:52px;
display:flex;align-items:center;justify-content:center;gap:6px;
background:rgba(10,18,32,0.92);border-bottom:1px solid rgba(79,195,247,0.15);
backdrop-filter:blur(10px);z-index:10;padding:0 10px;
}
.tab{
display:flex;align-items:center;gap:6px;
padding:8px 14px;border-radius:10px;cursor:pointer;
font-size:12px;font-weight:600;letter-spacing:0.3px;
border:1px solid transparent;transition:all 0.3s;
color:#94a3b8;white-space:nowrap;
}
.tab:hover{background:rgba(79,195,247,0.06);color:#cbd5e1}
.tab.active{
background:rgba(79,195,247,0.1);
border-color:rgba(79,195,247,0.3);color:#e2e8f0;
}
.tab .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
/* ── Info panel ── */
#info{
position:absolute;top:64px;right:14px;width:290px;
background:rgba(10,18,32,0.94);border:1px solid rgba(79,195,247,0.2);
border-radius:14px;padding:18px 20px;
backdrop-filter:blur(14px);box-shadow:0 8px 32px rgba(0,0,0,0.5);
font-size:12px;line-height:1.7;z-index:5;
transition:opacity 0.4s;
}
#info h3{color:#4FC3F7;font-size:13px;margin-bottom:10px;text-transform:uppercase;letter-spacing:1px;text-align:center}
.row{display:flex;justify-content:space-between;padding:2px 0}
.row .lbl{color:#64748b}.row .val{color:#cbd5e1;font-weight:600;text-align:right;max-width:160px}
.sep{border:none;border-top:1px solid rgba(79,195,247,0.1);margin:7px 0}
.tag{display:inline-block;background:rgba(79,195,247,0.08);border:1px solid rgba(79,195,247,0.18);
border-radius:6px;padding:1px 7px;margin:2px;font-size:10.5px;color:#4FC3F7}
.fact{background:rgba(79,195,247,0.05);border-radius:8px;padding:8px 10px;margin-top:6px}
.fact b{color:#4FC3F7;font-size:11px}
.fact p{color:#cbd5e1;font-size:11px;margin-top:3px;line-height:1.5}
/* ── Scene label ── */
#sceneLabel{
position:absolute;bottom:14px;left:50%;transform:translateX(-50%);
background:rgba(10,18,32,0.85);border:1px solid rgba(79,195,247,0.15);
border-radius:10px;padding:8px 18px;font-size:11px;color:#94a3b8;
backdrop-filter:blur(8px);text-align:center;z-index:5;
pointer-events:none;transition:opacity 0.3s;
}
#sceneLabel span{color:#e2e8f0;font-weight:700}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="tabs"></div>
<div id="info"><h3>🛰️ Satellite Missions</h3><p style="color:#64748b;text-align:center;padding:12px 0">Select a mission type above<br>to explore its animation.</p></div>
<div id="sceneLabel"><span>Mission Control</span> — choose a category</div>
<script>
// ═══════════════════════════════════════════════════════════════
// DATA
// ═══════════════════════════════════════════════════════════════
const cats=[
{id:'nav',icon:'🧭',name:'Navigation',color:'#34d399',rgb:'52,211,153',
orbit:'MEO',alt:'20 200 km',band:'L-band (1.21.6 GHz)',power:'~50 W/signal',
precision:'< 1 m (dual-freq)',lifetime:'1215 yrs',
examples:['GPS (USA)','Galileo (EU)','GLONASS (RU)','BeiDou (CN)'],
fact:'Each GPS satellite carries 4 atomic clocks accurate to ~1 ns. Relativity corrections of 38 μs/day are applied.',
sceneHint:'Triangulation — 3 satellites fix your position'},
{id:'com',icon:'📡',name:'Communication',color:'#4FC3F7',rgb:'79,195,247',
orbit:'GEO + LEO',alt:'55036 000 km',band:'C / Ku / Ka-band',power:'220 kW',
precision:'100+ Gbps (HTS)',lifetime:'1520 yrs',
examples:['Starlink','Intelsat','SES','OneWeb'],
fact:'A modern HTS can deliver 500+ Gbps — equivalent to 100 000 HD streams simultaneously.',
sceneHint:'Data flowing between ground stations'},
{id:'eo',icon:'📸',name:'Earth Observation',color:'#a78bfa',rgb:'167,139,250',
orbit:'LEO (SSO)',alt:'500800 km',band:'X-band (downlink)',power:'SAR + optical',
precision:'0.330 m resolution',lifetime:'57 yrs',
examples:['Sentinel','Landsat','Planet Doves','Pléiades'],
fact:'Planet Labs\' 200+ Doves image the entire land surface every single day at 3 m resolution.',
sceneHint:'Satellite scanning the terrain below'},
{id:'wx',icon:'🌦️',name:'Weather',color:'#fbbf24',rgb:'251,191,36',
orbit:'GEO + LEO',alt:'80036 000 km',band:'L / S / Ka-band',power:'Imager + Sounder',
precision:'Full disk every 10 min',lifetime:'1015 yrs',
examples:['Meteosat','GOES','Himawari','FengYun'],
fact:'GOES-16 produces 3.6 TB of weather imagery per day across 16 spectral bands.',
sceneHint:'Atmospheric monitoring & cloud tracking'},
{id:'sci',icon:'🔭',name:'Science',color:'#f472b6',rgb:'244,114,182',
orbit:'Various (L2, LEO, HEO)',alt:'540 km 1.5M km',band:'S / X / Ka-band',power:'Telescope + Spectrometer',
precision:'28 Mbps (JWST)',lifetime:'520+ yrs',
examples:['JWST','Hubble','Gaia','SOHO','Chandra'],
fact:'JWST\'s 6.5 m gold mirror operates at 233 °C to see infrared light from the first galaxies, 13.5 billion years ago.',
sceneHint:'Deep-space telescope view'},
{id:'def',icon:'🛡️',name:'Defense',color:'#fb923c',rgb:'251,146,60',
orbit:'LEO / GEO / HEO',alt:'Variable',band:'UHF / SHF / EHF',power:'Anti-jam, encrypted',
precision:'SIGINT + IMINT',lifetime:'715 yrs',
examples:['SBIRS','WGS','Syracuse (FR)','Skynet (UK)'],
fact:'SBIRS infrared satellites detect missile launches within seconds from 36 000 km away by spotting the heat plume.',
sceneHint:'Tactical radar sweep'},
];
// ═══════════════════════════════════════════════════════════════
// CANVAS
// ═══════════════════════════════════════════════════════════════
const cv=document.getElementById('c'),cx=cv.getContext('2d');
let W,H;
function resize(){W=cv.width=window.innerWidth;H=cv.height=window.innerHeight}
window.addEventListener('resize',resize);resize();
let activeCat=null,t=0;
// Stars
const stars=Array.from({length:140},()=>({x:Math.random(),y:Math.random(),r:Math.random()*1+0.3,ph:Math.random()*6.28,sp:0.4+Math.random()*1.2}));
// ═══════════════════════════════════════════════════════════════
// BUILD TABS
// ═══════════════════════════════════════════════════════════════
const tabsEl=document.getElementById('tabs');
cats.forEach(c=>{
const d=document.createElement('div');
d.className='tab';d.dataset.id=c.id;
d.innerHTML='<span class="dot" style="background:'+c.color+'"></span>'+c.icon+' '+c.name;
d.onclick=()=>{
activeCat=activeCat===c.id?null:c.id;
document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active',t.dataset.id===activeCat));
if(activeCat)showInfo(c);else resetInfo();
updateLabel();
};
tabsEl.appendChild(d);
});
function resetInfo(){
document.getElementById('info').innerHTML='<h3>🛰️ Satellite Missions</h3><p style="color:#64748b;text-align:center;padding:12px 0">Select a mission type above<br>to explore its animation.</p>';
}
function showInfo(c){
document.getElementById('info').innerHTML=
'<h3>'+c.icon+' '+c.name+'</h3>'+
'<div class="row"><span class="lbl">Orbit</span><span class="val">'+c.orbit+'</span></div>'+
'<div class="row"><span class="lbl">Altitude</span><span class="val">'+c.alt+'</span></div>'+
'<div class="row"><span class="lbl">Band</span><span class="val">'+c.band+'</span></div>'+
'<div class="row"><span class="lbl">Power / Payload</span><span class="val">'+c.power+'</span></div>'+
'<div class="row"><span class="lbl">Performance</span><span class="val">'+c.precision+'</span></div>'+
'<div class="row"><span class="lbl">Lifetime</span><span class="val">'+c.lifetime+'</span></div>'+
'<hr class="sep">'+
'<div style="color:#64748b;font-size:10.5px;margin-bottom:4px">Notable systems:</div>'+
'<div>'+c.examples.map(function(e){return '<span class="tag">'+e+'</span>'}).join('')+'</div>'+
'<hr class="sep">'+
'<div class="fact"><b>💡 Did you know?</b><p>'+c.fact+'</p></div>';
}
function updateLabel(){
var el=document.getElementById('sceneLabel');
if(!activeCat){el.innerHTML='<span>Mission Control</span> — choose a category';return}
var c=cats.find(function(x){return x.id===activeCat});
el.innerHTML='<span>'+c.icon+' '+c.name+'</span> — '+c.sceneHint;
}
// ═══════════════════════════════════════════════════════════════
// SCENE HELPERS
// ═══════════════════════════════════════════════════════════════
var sceneX=0, sceneY=52;
function sceneW(){return W-310}
function sceneH(){return H-52}
function drawBg(){
var g=cx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#060d19');g.addColorStop(1,'#0a1628');
cx.fillStyle=g;cx.fillRect(0,0,W,H);
stars.forEach(function(s){
var a=0.2+Math.sin(t*s.sp+s.ph)*0.3+0.3;
cx.beginPath();cx.arc(s.x*W,s.y*H,s.r,0,6.28);
cx.fillStyle='rgba(255,255,255,'+a+')';cx.fill();
});
}
// ═══════════════════════════════════════════════════════════════
// SCENE: NAVIGATION — GPS triangulation
// ═══════════════════════════════════════════════════════════════
var navReceiverPath=[];
for(var ni=0;ni<200;ni++){
navReceiverPath.push({fx:0.35+Math.sin(ni*0.04)*0.18, fy:0.7+Math.cos(ni*0.06)*0.1});
}
var navIdx=0;
function drawNav(){
var sw=sceneW(),sh=sceneH(),ox=sceneX,oy=sceneY;
var col='52,211,153';
// Ground / horizon
cx.fillStyle='rgba(10,30,20,0.5)';
cx.fillRect(ox,oy+sh*0.65,sw,sh*0.35);
cx.strokeStyle='rgba(52,211,153,0.15)';cx.lineWidth=1;
cx.beginPath();cx.moveTo(ox,oy+sh*0.65);cx.lineTo(ox+sw,oy+sh*0.65);cx.stroke();
// Grid on ground
cx.strokeStyle='rgba(52,211,153,0.06)';
for(var gi=0;gi<20;gi++){
var gx=ox+(sw/20)*gi;
cx.beginPath();cx.moveTo(gx,oy+sh*0.65);cx.lineTo(gx,oy+sh);cx.stroke();
}
for(var gj=0;gj<6;gj++){
var gy=oy+sh*0.65+((sh*0.35)/6)*gj;
cx.beginPath();cx.moveTo(ox,gy);cx.lineTo(ox+sw,gy);cx.stroke();
}
// 3 satellites positions
var sats=[
{x:ox+sw*0.18,y:oy+sh*0.1},{x:ox+sw*0.52,y:oy+sh*0.06},{x:ox+sw*0.82,y:oy+sh*0.15}
];
// Receiver
navIdx=(navIdx+0.3)%navReceiverPath.length;
var rp=navReceiverPath[Math.floor(navIdx)];
var rx=ox+sw*rp.fx,ry=oy+sh*rp.fy;
// Draw range circles from each sat
sats.forEach(function(s,i){
var dist=Math.sqrt((s.x-rx)*(s.x-rx)+(s.y-ry)*(s.y-ry));
// Fixed-speed expanding circles (maxR = constant, not dist-dependent)
var maxR=280;
var speed=35;
for(var r=0;r<3;r++){
var cr=((t*speed+i*93+r*(maxR/3))%maxR);
var alpha=cr<dist ? 0.22*(1-cr/maxR) : 0.06*(1-cr/maxR);
cx.beginPath();cx.arc(s.x,s.y,cr,0,6.28);
cx.strokeStyle='rgba('+col+','+alpha+')';cx.lineWidth=1.5;cx.stroke();
}
var lineAlpha=0.08+Math.sin(t*2+i)*0.05;
cx.beginPath();cx.moveTo(s.x,s.y);cx.lineTo(rx,ry);
cx.strokeStyle='rgba('+col+','+lineAlpha+')';cx.lineWidth=0.8;
cx.setLineDash([4,6]);cx.stroke();cx.setLineDash([]);
cx.save();
cx.shadowColor='rgba('+col+',0.6)';cx.shadowBlur=12;
cx.beginPath();cx.arc(s.x,s.y,7,0,6.28);
cx.fillStyle='rgba('+col+',0.9)';cx.fill();
cx.restore();
cx.strokeStyle='rgba('+col+',0.5)';cx.lineWidth=2;
cx.beginPath();cx.moveTo(s.x-12,s.y);cx.lineTo(s.x+12,s.y);cx.stroke();
cx.fillStyle='rgba('+col+',0.7)';cx.font='bold 10px system-ui';cx.textAlign='center';
cx.fillText('SV'+(i+1),s.x,s.y-14);
});
// Receiver
cx.save();
cx.shadowColor='rgba(52,211,153,0.8)';cx.shadowBlur=16;
cx.beginPath();cx.arc(rx,ry,6,0,6.28);
cx.fillStyle='#34d399';cx.fill();
cx.restore();
cx.strokeStyle='rgba(52,211,153,0.6)';cx.lineWidth=1;
cx.beginPath();cx.moveTo(rx-12,ry);cx.lineTo(rx+12,ry);cx.moveTo(rx,ry-12);cx.lineTo(rx,ry+12);cx.stroke();
cx.beginPath();cx.arc(rx,ry,10,0,6.28);cx.stroke();
cx.fillStyle='#34d399';cx.font='bold 10px monospace';cx.textAlign='center';
var lat=(rp.fy*90-20).toFixed(2),lon=(rp.fx*360-90).toFixed(2);
cx.fillText(lat+'°N '+lon+'°E',rx,ry+24);
cx.font='9px system-ui';cx.fillStyle='rgba(52,211,153,0.6)';
cx.fillText('Fix: 3D — Accuracy: 0.8 m',rx,ry+36);
cx.textAlign='start';
}
// ═══════════════════════════════════════════════════════════════
// SCENE: COMMUNICATION — Network data flow
// ═══════════════════════════════════════════════════════════════
var comNodes=[];
var comLinks=[];
var comPackets=[];
var comInit=false;
function initCom(){
if(comInit)return;comInit=true;
var sw=sceneW(),sh=sceneH(),ox=sceneX,oy=sceneY;
var positions=[
{fx:0.08,fy:0.4,label:'New York'},{fx:0.25,fy:0.25,label:'London'},
{fx:0.38,fy:0.35,label:'Paris'},{fx:0.55,fy:0.22,label:'Moscow'},
{fx:0.7,fy:0.45,label:'Dubai'},{fx:0.82,fy:0.3,label:'Tokyo'},
{fx:0.6,fy:0.7,label:'Mumbai'},{fx:0.2,fy:0.65,label:'São Paulo'},
{fx:0.45,fy:0.6,label:'Nairobi'},{fx:0.9,fy:0.6,label:'Sydney'},
];
positions.forEach(function(p){
comNodes.push({x:ox+sw*p.fx,y:oy+sh*p.fy,label:p.label,pulse:Math.random()*6.28});
});
var pairs=[[0,1],[1,2],[2,3],[3,5],[4,5],[4,6],[6,8],[8,7],[7,0],[1,4],[2,8],[5,9],[6,9],[0,7],[3,4]];
pairs.forEach(function(pr){comLinks.push({a:pr[0],b:pr[1]})});
for(var pi=0;pi<20;pi++){
var l=comLinks[Math.floor(Math.random()*comLinks.length)];
comPackets.push({link:l,progress:Math.random(),speed:0.003+Math.random()*0.004,forward:Math.random()>0.5});
}
}
function drawCom(){
initCom();
var col='79,195,247';
comLinks.forEach(function(l){
var a=comNodes[l.a],b=comNodes[l.b];
var mx=(a.x+b.x)/2,my=(a.y+b.y)/2-30;
cx.beginPath();cx.moveTo(a.x,a.y);cx.quadraticCurveTo(mx,my,b.x,b.y);
cx.strokeStyle='rgba('+col+',0.1)';cx.lineWidth=1;cx.stroke();
});
comPackets.forEach(function(p){
p.progress+=p.speed*(p.forward?1:-1);
if(p.progress>1||p.progress<0){
p.forward=!p.forward;
p.progress=Math.max(0,Math.min(1,p.progress));
}
var a=comNodes[p.link.a],b=comNodes[p.link.b];
var mx=(a.x+b.x)/2,my=(a.y+b.y)/2-30;
var tt=p.progress;
var px=(1-tt)*(1-tt)*a.x+2*(1-tt)*tt*mx+tt*tt*b.x;
var py=(1-tt)*(1-tt)*a.y+2*(1-tt)*tt*my+tt*tt*b.y;
cx.save();
cx.shadowColor='rgba('+col+',0.7)';cx.shadowBlur=6;
cx.beginPath();cx.arc(px,py,2.5,0,6.28);
cx.fillStyle='rgba('+col+',0.85)';cx.fill();
cx.restore();
});
comNodes.forEach(function(n){
var pr=8+Math.sin(t*2+n.pulse)*3;
cx.beginPath();cx.arc(n.x,n.y,pr,0,6.28);
cx.strokeStyle='rgba('+col+',0.15)';cx.lineWidth=1;cx.stroke();
cx.save();
cx.shadowColor='rgba('+col+',0.5)';cx.shadowBlur=10;
cx.beginPath();cx.arc(n.x,n.y,5,0,6.28);
cx.fillStyle='rgba('+col+',0.9)';cx.fill();
cx.restore();
cx.fillStyle='rgba('+col+',0.65)';cx.font='9px system-ui';cx.textAlign='center';
cx.fillText(n.label,n.x,n.y+18);
});
cx.fillStyle='rgba(79,195,247,0.4)';cx.font='11px monospace';cx.textAlign='start';
var tp=Math.floor(280+Math.sin(t)*40);
cx.fillText('Aggregate throughput: '+tp+' Gbps',sceneX+16,sceneY+sceneH()-20);
cx.textAlign='start';
}
// ═══════════════════════════════════════════════════════════════
// SCENE: EARTH OBSERVATION — Scanning terrain
// ═══════════════════════════════════════════════════════════════
function drawEO(){
var sw=sceneW(),sh=sceneH(),ox=sceneX,oy=sceneY;
var col='167,139,250';
// Terrain
var groundY=oy+sh*0.7;
cx.fillStyle='rgba(30,20,50,0.5)';
cx.fillRect(ox,groundY,sw,sh*0.3);
cx.beginPath();cx.moveTo(ox,groundY);
for(var x=0;x<sw;x+=4){
var h=Math.sin(x*0.01)*15+Math.sin(x*0.035+2)*8+Math.cos(x*0.007)*12;
cx.lineTo(ox+x,groundY-h);
}
cx.lineTo(ox+sw,groundY+10);cx.lineTo(ox,groundY+10);cx.closePath();
cx.fillStyle='rgba(60,40,90,0.4)';cx.fill();
cx.strokeStyle='rgba('+col+',0.2)';cx.lineWidth=1;cx.stroke();
cx.strokeStyle='rgba('+col+',0.04)';
for(var gx=0;gx<sw;gx+=30){cx.beginPath();cx.moveTo(ox+gx,groundY-20);cx.lineTo(ox+gx,oy+sh);cx.stroke();}
for(var gy=groundY;gy<oy+sh;gy+=20){cx.beginPath();cx.moveTo(ox,gy);cx.lineTo(ox+sw,gy);cx.stroke();}
var satX=ox+((t*25)%sw);
var satY=oy+sh*0.12;
// Scan beam
var beamW=40;
cx.beginPath();
cx.moveTo(satX,satY+8);
cx.lineTo(satX-beamW,groundY-15);
cx.lineTo(satX+beamW,groundY-15);
cx.closePath();
var beamG=cx.createLinearGradient(satX,satY,satX,groundY);
beamG.addColorStop(0,'rgba('+col+',0.25)');
beamG.addColorStop(1,'rgba('+col+',0.02)');
cx.fillStyle=beamG;cx.fill();
cx.beginPath();
cx.moveTo(satX-beamW,groundY-15);
cx.lineTo(satX+beamW,groundY-15);
cx.strokeStyle='rgba('+col+',0.7)';cx.lineWidth=2;cx.stroke();
// Image strips
var stripY=oy+sh*0.45;
var stripH=sh*0.18;
var scannedW=satX-ox-beamW;
if(scannedW>0){
var bands=['rgba(80,200,120,0.15)','rgba(200,80,80,0.12)','rgba(80,80,200,0.12)'];
bands.forEach(function(b,i){
cx.fillStyle=b;
cx.fillRect(ox,stripY+i*(stripH/3),scannedW,stripH/3);
});
cx.strokeStyle='rgba('+col+',0.2)';cx.lineWidth=0.5;
cx.strokeRect(ox,stripY,scannedW,stripH);
cx.font='9px monospace';cx.textAlign='start';
['NIR','RED','BLUE'].forEach(function(b,i){
cx.fillStyle='rgba('+col+',0.5)';
cx.fillText(b,ox+4,stripY+i*(stripH/3)+12);
});
}
// Satellite body
cx.save();
cx.shadowColor='rgba('+col+',0.7)';cx.shadowBlur=14;
cx.fillStyle='rgba('+col+',0.9)';
cx.fillRect(satX-6,satY-4,12,8);
cx.restore();
cx.fillStyle='rgba('+col+',0.4)';
cx.fillRect(satX-22,satY-2,14,4);
cx.fillRect(satX+8,satY-2,14,4);
cx.beginPath();cx.arc(satX,satY+5,3,0,6.28);
cx.fillStyle='rgba('+col+',0.8)';cx.fill();
cx.fillStyle='rgba('+col+',0.6)';cx.font='bold 10px system-ui';cx.textAlign='center';
cx.fillText('Sentinel-2A — SSO 786 km',satX,satY-14);
cx.fillStyle='rgba('+col+',0.4)';cx.font='10px monospace';cx.textAlign='start';
cx.fillText('GSD: 10 m | Swath: 290 km | Bands: 13',ox+16,oy+sh-20);
cx.textAlign='start';
}
// ═══════════════════════════════════════════════════════════════
// SCENE: WEATHER — Atmospheric monitoring
// ═══════════════════════════════════════════════════════════════
var wxClouds=Array.from({length:18},function(){return{
x:Math.random(),y:0.3+Math.random()*0.4,
r:20+Math.random()*40,sp:0.1+Math.random()*0.3,
opacity:0.15+Math.random()*0.2
}});
var wxRaindrops=Array.from({length:60},function(){return{
x:Math.random(),y:Math.random(),sp:2+Math.random()*3,len:4+Math.random()*8
}});
function drawWx(){
var sw=sceneW(),sh=sceneH(),ox=sceneX,oy=sceneY;
var col='251,191,36';
var tg=cx.createLinearGradient(ox,oy,ox,oy+sh);
tg.addColorStop(0,'rgba(30,10,60,0.3)');
tg.addColorStop(0.4,'rgba(10,20,50,0.3)');
tg.addColorStop(1,'rgba(10,30,40,0.3)');
cx.fillStyle=tg;cx.fillRect(ox,oy,sw,sh);
// Isobars
cx.strokeStyle='rgba(251,191,36,0.06)';cx.lineWidth=1;
for(var ii=0;ii<8;ii++){
cx.beginPath();
var baseY=oy+sh*0.2+ii*(sh*0.08);
cx.moveTo(ox,baseY);
for(var ix=0;ix<sw;ix+=5){
var iy=baseY+Math.sin((ix+t*20)*0.008+ii)*15;
cx.lineTo(ox+ix,iy);
}
cx.stroke();
if(ii%2===0){
cx.fillStyle='rgba(251,191,36,0.15)';cx.font='8px monospace';
cx.fillText((1013-ii*4)+'hPa',ox+sw-60,baseY-3);
}
}
// Clouds
wxClouds.forEach(function(c){
c.x+=c.sp*0.0003;
if(c.x>1.15)c.x=-0.15;
var cloudX=ox+sw*c.x,cloudY=oy+sh*c.y;
cx.fillStyle='rgba(200,210,230,'+c.opacity+')';
for(var ci=0;ci<4;ci++){
cx.beginPath();
cx.arc(cloudX+ci*c.r*0.4-c.r*0.6, cloudY+Math.sin(ci*1.5)*c.r*0.15, c.r*0.4,0,6.28);
cx.fill();
}
});
// Rain
cx.strokeStyle='rgba(100,180,255,0.3)';cx.lineWidth=1;
wxRaindrops.forEach(function(d){
d.y+=d.sp*0.003;
if(d.y>1)d.y=0.4;
if(d.y>0.5){
var dx=ox+sw*d.x,dy=oy+sh*d.y;
cx.beginPath();cx.moveTo(dx,dy);cx.lineTo(dx-1,dy+d.len);cx.stroke();
}
});
// GEO satellite
var satX=ox+sw*0.5,satY=oy+30;
cx.save();cx.shadowColor='rgba('+col+',0.6)';cx.shadowBlur=10;
cx.beginPath();cx.arc(satX,satY,6,0,6.28);
cx.fillStyle='rgba('+col+',0.9)';cx.fill();cx.restore();
cx.strokeStyle='rgba('+col+',0.4)';cx.lineWidth=2;
cx.beginPath();cx.moveTo(satX-14,satY);cx.lineTo(satX+14,satY);cx.stroke();
// Scan swath
cx.beginPath();
cx.moveTo(satX,satY+8);
cx.lineTo(ox+sw*0.1,oy+sh*0.9);
cx.lineTo(ox+sw*0.9,oy+sh*0.9);
cx.closePath();
var sg=cx.createLinearGradient(satX,satY,satX,oy+sh);
sg.addColorStop(0,'rgba('+col+',0.08)');sg.addColorStop(1,'rgba('+col+',0.01)');
cx.fillStyle=sg;cx.fill();
// Rotating scan line
var scanAngle=(t*0.5)%(Math.PI*2);
var scanEndX=satX+Math.sin(scanAngle)*sw*0.4;
var scanEndY=satY+Math.abs(Math.cos(scanAngle))*sh*0.85;
cx.beginPath();cx.moveTo(satX,satY);cx.lineTo(scanEndX,scanEndY);
cx.strokeStyle='rgba('+col+',0.3)';cx.lineWidth=1.5;cx.stroke();
// Temperature scale
cx.fillStyle='rgba('+col+',0.5)';cx.font='9px monospace';cx.textAlign='start';
var temps=['-60°C','-30°C','0°C','+20°C','+35°C'];
temps.forEach(function(tmp,i){
var ty=oy+sh*0.15+i*(sh*0.17);
cx.fillText(tmp,ox+8,ty);
});
cx.fillStyle='rgba('+col+',0.6)';cx.font='bold 10px system-ui';cx.textAlign='center';
cx.fillText('Meteosat — GEO 36 000 km — Full Disk Imaging',satX,satY-12);
cx.textAlign='start';
cx.fillStyle='rgba('+col+',0.35)';cx.font='10px monospace';
cx.fillText('Update: 10 min | 16 bands | 3.6 TB/day',ox+16,oy+sh-20);
}
// ═══════════════════════════════════════════════════════════════
// SCENE: SCIENCE — Deep space telescope view
// ═══════════════════════════════════════════════════════════════
var sciStars=Array.from({length:300},function(){return{
x:Math.random(),y:Math.random(),
r:Math.random()*1.6+0.2,
hue:Math.random()*60+200,
br:0.3+Math.random()*0.7,
twinkle:Math.random()*6.28
}});
var sciNebula=Array.from({length:12},function(){return{
x:0.3+Math.random()*0.4,y:0.3+Math.random()*0.4,
r:40+Math.random()*80,
hue:Math.random()*360,
alpha:0.03+Math.random()*0.04
}});
function drawSci(){
var sw=sceneW(),sh=sceneH(),ox=sceneX,oy=sceneY;
var col='244,114,182';
cx.fillStyle='rgba(3,3,12,0.6)';cx.fillRect(ox,oy,sw,sh);
sciNebula.forEach(function(n){
var nx=ox+sw*n.x,ny=oy+sh*n.y;
var grad=cx.createRadialGradient(nx,ny,0,nx,ny,n.r);
grad.addColorStop(0,'hsla('+n.hue+',70%,50%,'+(n.alpha+Math.sin(t*0.5+n.hue)*0.01)+')');
grad.addColorStop(1,'transparent');
cx.fillStyle=grad;
cx.fillRect(nx-n.r,ny-n.r,n.r*2,n.r*2);
});
sciStars.forEach(function(s){
var alpha=s.br*(0.5+Math.sin(t*1.5+s.twinkle)*0.5);
cx.beginPath();
cx.arc(ox+sw*s.x,oy+sh*s.y,s.r,0,6.28);
cx.fillStyle='hsla('+s.hue+',60%,80%,'+alpha+')';
cx.fill();
});
// Telescope reticle
var rX=ox+sw*0.42,rY=oy+sh*0.45;
var rR=sh*0.28;
cx.strokeStyle='rgba('+col+',0.15)';cx.lineWidth=0.8;
cx.beginPath();cx.arc(rX,rY,rR,0,6.28);cx.stroke();
cx.beginPath();cx.arc(rX,rY,rR*0.6,0,6.28);cx.stroke();
cx.beginPath();cx.moveTo(rX-rR,rY);cx.lineTo(rX+rR,rY);cx.stroke();
cx.beginPath();cx.moveTo(rX,rY-rR);cx.lineTo(rX,rY+rR);cx.stroke();
[0,Math.PI/2,Math.PI,3*Math.PI/2].forEach(function(a){
var ix=rX+Math.cos(a)*rR,iy=rY+Math.sin(a)*rR;
cx.beginPath();
cx.moveTo(ix,iy);cx.lineTo(ix+Math.cos(a)*10,iy+Math.sin(a)*10);
cx.strokeStyle='rgba('+col+',0.4)';cx.lineWidth=2;cx.stroke();
});
// Galaxy in reticle
var gpulse=0.7+Math.sin(t*1.2)*0.3;
var galGrad=cx.createRadialGradient(rX,rY,0,rX,rY,28*gpulse);
galGrad.addColorStop(0,'rgba(255,200,100,'+(0.3*gpulse)+')');
galGrad.addColorStop(0.4,'rgba('+col+','+(0.15*gpulse)+')');
galGrad.addColorStop(1,'transparent');
cx.fillStyle=galGrad;
cx.fillRect(rX-40,rY-40,80,80);
// Spiral arms
cx.save();cx.translate(rX,rY);cx.rotate(t*0.1);
cx.strokeStyle='rgba(255,200,140,'+(0.1*gpulse)+')';cx.lineWidth=2;
cx.beginPath();
for(var sa=0;sa<Math.PI*3;sa+=0.1){
var sr=3+sa*5;
cx.lineTo(Math.cos(sa)*sr,Math.sin(sa)*sr);
}
cx.stroke();
cx.beginPath();
for(var sa2=0;sa2<Math.PI*3;sa2+=0.1){
var sr2=3+sa2*5;
cx.lineTo(-Math.cos(sa2)*sr2,-Math.sin(sa2)*sr2);
}
cx.stroke();
cx.restore();
// Data readout
cx.fillStyle='rgba('+col+',0.5)';cx.font='9px monospace';cx.textAlign='start';
var lines=[
'Target: SMACS 0723 — z = 7.66',
'RA: 07h 23m 19.5s Dec: -73° 27\' 15"',
'Exposure: NIRCam F200W — 12.5 hrs',
'Signal: '+(14+Math.sin(t)*2).toFixed(1)+' e\u207B/s/px',
];
lines.forEach(function(l,i){
cx.fillText(l,ox+16,oy+sh-60+i*14);
});
cx.fillStyle='rgba('+col+',0.6)';cx.font='bold 10px system-ui';cx.textAlign='center';
cx.fillText('JWST — NIRCam Deep Field — L2 Lagrange Point',ox+sw*0.42,oy+20);
cx.textAlign='start';
}
// ═══════════════════════════════════════════════════════════════
// SCENE: DEFENSE — Tactical radar sweep
// ═══════════════════════════════════════════════════════════════
var defBlips=Array.from({length:12},function(){return{
angle:Math.random()*6.28,
dist:0.2+Math.random()*0.7,
type:['friendly','hostile','unknown'][Math.floor(Math.random()*3)],
label:['F-16','MiG-29','UAV','Ship','Sub','SAM','AWACS','Tanker','C2','Helo','Drone','Cargo'][Math.floor(Math.random()*12)],
blinkPhase:Math.random()*6.28
}});
function drawDef(){
var sw=sceneW(),sh=sceneH(),ox=sceneX,oy=sceneY;
var col='251,146,60';
var rcx2=ox+sw*0.42,rcy2=oy+sh*0.52;
var rr=Math.min(sw,sh)*0.38;
cx.fillStyle='rgba(5,15,5,0.4)';
cx.beginPath();cx.arc(rcx2,rcy2,rr+10,0,6.28);cx.fill();
for(var ri=1;ri<=4;ri++){
cx.beginPath();cx.arc(rcx2,rcy2,rr*ri/4,0,6.28);
cx.strokeStyle='rgba('+col+','+(0.08+ri*0.02)+')';cx.lineWidth=0.8;cx.stroke();
cx.fillStyle='rgba('+col+',0.3)';cx.font='8px monospace';
cx.fillText(ri*100+'km',rcx2+rr*ri/4+4,rcy2-3);
}
cx.strokeStyle='rgba('+col+',0.07)';cx.lineWidth=0.5;
cx.beginPath();cx.moveTo(rcx2-rr,rcy2);cx.lineTo(rcx2+rr,rcy2);cx.stroke();
cx.beginPath();cx.moveTo(rcx2,rcy2-rr);cx.lineTo(rcx2,rcy2+rr);cx.stroke();
cx.beginPath();cx.moveTo(rcx2-rr*0.707,rcy2-rr*0.707);cx.lineTo(rcx2+rr*0.707,rcy2+rr*0.707);cx.stroke();
cx.beginPath();cx.moveTo(rcx2+rr*0.707,rcy2-rr*0.707);cx.lineTo(rcx2-rr*0.707,rcy2+rr*0.707);cx.stroke();
// Sweep
var sweepAngle=(t*0.8)%(Math.PI*2);
cx.save();
cx.translate(rcx2,rcy2);
cx.rotate(sweepAngle);
for(var si=0;si<40;si++){
var sa=-si*0.02;
var salpha=0.25*(1-si/40);
cx.beginPath();
cx.moveTo(0,0);
cx.lineTo(Math.cos(sa)*rr,Math.sin(sa)*rr);
cx.strokeStyle='rgba('+col+','+salpha+')';cx.lineWidth=1;cx.stroke();
}
cx.beginPath();cx.moveTo(0,0);cx.lineTo(rr,0);
cx.strokeStyle='rgba('+col+',0.7)';cx.lineWidth=2;cx.stroke();
cx.restore();
// Blips
defBlips.forEach(function(b){
var bx=rcx2+Math.cos(b.angle)*b.dist*rr;
var by=rcy2+Math.sin(b.angle)*b.dist*rr;
var angleDiff=(sweepAngle-b.angle+Math.PI*4)%(Math.PI*2);
var vis=angleDiff<Math.PI*1.5 ? Math.max(0,1-angleDiff/(Math.PI*1.5)) : 0;
if(vis<0.05)return;
var colors={friendly:'rgba(52,211,153,',hostile:'rgba(239,68,68,',unknown:'rgba(251,191,36,'};
var bc=colors[b.type];
cx.save();
cx.globalAlpha=vis;
cx.shadowColor=bc+'0.8)';cx.shadowBlur=8;
cx.beginPath();cx.arc(bx,by,3.5,0,6.28);
cx.fillStyle=bc+'0.9)';cx.fill();
cx.restore();
if(vis>0.4){
cx.globalAlpha=vis*0.7;
cx.fillStyle=bc+'0.7)';cx.font='8px monospace';cx.textAlign='center';
cx.fillText(b.label,bx,by-10);
cx.globalAlpha=1;
}
});
cx.globalAlpha=1;
// Legend
cx.font='9px system-ui';cx.textAlign='start';
var legendY=oy+sh-55;
[['● Friendly','rgba(52,211,153,0.7)'],['● Hostile','rgba(239,68,68,0.7)'],['● Unknown','rgba(251,191,36,0.7)']].forEach(function(item,i){
cx.fillStyle=item[1];cx.fillText(item[0],ox+16,legendY+i*14);
});
cx.fillStyle='rgba('+col+',0.5)';cx.font='bold 10px system-ui';cx.textAlign='center';
cx.fillText('SBIRS / EW — Surveillance & Early Warning',rcx2,oy+20);
cx.font='9px monospace';cx.fillStyle='rgba('+col+',0.35)';
cx.fillText('Sweep rate: 7.6 RPM | Range: 400 km | Tracks: '+defBlips.length,rcx2,oy+sh-20);
cx.textAlign='start';
}
// ═══════════════════════════════════════════════════════════════
// IDLE SCENE — no category selected
// ═══════════════════════════════════════════════════════════════
function drawIdle(){
var sw=sceneW(),sh=sceneH(),ox=sceneX,oy=sceneY;
var cx0=ox+sw*0.42,cy0=oy+sh*0.5;
var hexR=Math.min(sw,sh)*0.22;
cats.forEach(function(ic,i){
var angle=i*(Math.PI*2/cats.length)-Math.PI/2+t*0.15;
var ix=cx0+Math.cos(angle)*hexR;
var iy=cy0+Math.sin(angle)*hexR+Math.sin(t*1.5+i)*8;
cx.beginPath();cx.arc(cx0,cy0,hexR,0,6.28);
cx.strokeStyle='rgba(79,195,247,0.04)';cx.lineWidth=0.8;cx.stroke();
cx.save();
cx.shadowColor='rgba('+ic.rgb+',0.4)';cx.shadowBlur=18;
cx.font='28px system-ui';cx.textAlign='center';cx.textBaseline='middle';
cx.fillText(ic.icon,ix,iy);
cx.restore();
cx.fillStyle=ic.color;cx.font='bold 10px system-ui';cx.textAlign='center';cx.textBaseline='top';
cx.fillText(ic.name,ix,iy+22);
cx.textBaseline='alphabetic';
});
cx.fillStyle='#64748b';cx.font='13px system-ui';cx.textAlign='center';
cx.fillText('Select a mission type to explore',cx0,cy0+4);
cx.textAlign='start';
}
// ═══════════════════════════════════════════════════════════════
// MAIN LOOP
// ═══════════════════════════════════════════════════════════════
var scenes={nav:drawNav,com:drawCom,eo:drawEO,wx:drawWx,sci:drawSci,def:drawDef};
function frame(){
t+=0.016;
cx.clearRect(0,0,W,H);
drawBg();
if(activeCat && scenes[activeCat]){
scenes[activeCat]();
} else {
drawIdle();
}
requestAnimationFrame(frame);
}
frame();
</script>
</body>
</html>
"""
def render():
"""Render the satellite missions / types dashboard page."""
st.markdown("## 🛰️ Satellite Missions — Types & Applications")
st.markdown(
"Explore **six categories** of satellite missions through unique animated scenes. "
"Click a tab to switch between navigation, communication, observation, weather, "
"science and defense — each with its own visual story."
)
st.divider()
components.html(_SATTYPES_HTML, height=720, scrolling=False)
# ── Educational content below ──
st.divider()
st.markdown("### 📋 Mission Comparison")
st.markdown("""
| Category | Orbit | Altitude | Key Band | Examples |
|:---|:---:|:---:|:---:|:---|
| 🧭 **Navigation** | MEO | 20 200 km | L-band | GPS, Galileo, GLONASS, BeiDou |
| 📡 **Communication** | GEO / LEO | 550 36 000 km | C / Ku / Ka | Starlink, Intelsat, SES |
| 📸 **Earth Observation** | LEO (SSO) | 500 800 km | X-band | Sentinel, Landsat, Planet |
| 🌦️ **Weather** | GEO / LEO | 800 36 000 km | L / Ka | Meteosat, GOES, Himawari |
| 🔭 **Science** | Various | Variable | S / X / Ka | JWST, Hubble, Gaia |
| 🛡️ **Defense** | LEO / GEO / HEO | Variable | UHF / EHF | SBIRS, WGS, Syracuse |
""")
col1, col2 = st.columns(2)
with col1:
with st.expander("🧭 Navigation Satellites"):
st.markdown("""
**How GNSS works:**
Each satellite broadcasts its precise position and exact time
(from onboard atomic clocks). A receiver picks up
signals from $\\geq 4$ satellites and solves the position equations:
$$d_i = c \\cdot (t_{\\text{rx}} - t_{\\text{tx},i})$$
Where $d_i$ is the pseudorange to satellite $i$, $c$ is the speed of
light, and $t_{\\text{rx}}$ is the receiver clock time.
With 4+ satellites the receiver solves for $(x, y, z, \\Delta t)$ —
3D position plus its own clock error.
**Key systems:**
- **GPS** (USA) — 31 sats, L1 / L2 / L5
- **Galileo** (EU) — 30 sats, E1 / E5a / E5b / E6
- **GLONASS** (Russia) — 24 sats
- **BeiDou** (China) — 35+ sats
""")
with st.expander("📡 Communication Satellites"):
st.markdown("""
**Evolution:**
1. **1960s** — Early Bird: 240 voice channels
2. **1980s** — Large GEO: thousands of transponders
3. **2000s** — HTS: spot beams, frequency reuse
4. **2020s** — Mega-constellations (Starlink 6 000+ LEO)
**Shannon connection:**
$$C = B \\cdot \\log_2\\!\\left(1 + \\frac{C}{N}\\right)$$
A GEO satellite at 36 000 km suffers ~50 dB more path loss than
LEO at 550 km, but compensates with larger antennas, higher power
and advanced modulation (DVB-S2X, 256-APSK).
""")
with st.expander("🌦️ Weather Satellites"):
st.markdown("""
**Two approaches:**
1. **GEO** (Meteosat, GOES, Himawari) — continuous monitoring,
full disk every 1015 min, 16+ spectral bands.
2. **LEO polar** (MetOp, NOAA) — higher resolution but 2 passes/day,
microwave sounders penetrate clouds.
**Data volume:** GOES-16 generates ~3.6 TB/day of imagery.
""")
with col2:
with st.expander("📸 Earth Observation Satellites"):
st.markdown("""
**Imaging technologies:**
| Type | Resolution | Use |
|:---|:---:|:---|
| **Optical** | 0.3 1 m | Urban mapping, defence |
| **Multispectral** | 5 30 m | Agriculture (NDVI) |
| **Hyperspectral** | 30 m, 200+ bands | Mineral detection |
| **SAR** | 1 10 m | All-weather imaging |
| **InSAR** | mm displacement | Ground subsidence |
**Sun-Synchronous Orbit (SSO):** ~98° inclination ensures
consistent solar illumination for temporal comparison.
""")
with st.expander("🔭 Science & Exploration"):
st.markdown("""
| Mission | Location | Purpose |
|:---|:---:|:---|
| **JWST** | L2 (1.5 M km) | IR astronomy |
| **Hubble** | LEO (540 km) | Optical / UV |
| **Gaia** | L2 | 3D map of 1.8 B stars |
| **Chandra** | HEO | X-ray astronomy |
**Deep space challenge:** Voyager 1 at 24 billion km transmits
at ~160 **bits**/s with 23 W through NASA's 70 m DSN antennas.
""")
with st.expander("🛡️ Defense & Intelligence"):
st.markdown("""
**Categories:**
- **MILCOM** — WGS, AEHF, Syracuse (secure links, EHF)
- **SIGINT** — intercept electromagnetic emissions
- **IMINT** — high-res optical/radar reconnaissance
- **Early Warning** — SBIRS IR missile detection from GEO
- **ELINT** — radar system characterisation
**Resilience:** frequency hopping, spread spectrum,
radiation hardening, satellite crosslinks.
""")

269
views/theory.py Normal file
View File

@@ -0,0 +1,269 @@
"""
Page 1: Shannon's Equation — Theoretical Exploration
Interactive exploration of the Shannon capacity formula with Plotly graphs.
"""
import streamlit as st
import numpy as np
import plotly.graph_objects as go
from math import log
from core.calculations import (
combine_cnr,
shannon_capacity,
shannon_points,
br_multiplier,
fmt_br,
)
from core.help_texts import THEORY_HELP
def _make_bw_sensitivity_plot(cnr_nyq: float, bw_nyq: float, c_n0: float) -> go.Figure:
"""Bandwidth sensitivity at constant power."""
n = 40
bw = np.zeros(n)
br = np.zeros(n)
cnr = np.zeros(n)
cnr[0] = cnr_nyq + 10 * log(8, 10)
bw[0] = bw_nyq / 8
br[0] = shannon_capacity(bw[0], cnr[0])
for i in range(1, n):
bw[i] = bw[i - 1] * 2 ** (1 / 6)
cnr[i] = cnr[i - 1] - 10 * log(bw[i] / bw[i - 1], 10)
br[i] = shannon_capacity(bw[i], cnr[i])
fig = go.Figure()
fig.add_trace(go.Scatter(
x=bw, y=br, mode="lines",
name="Shannon Capacity",
line=dict(color="#4FC3F7", width=3),
))
# Mark reference point
ref_br = shannon_capacity(bw_nyq, cnr_nyq)
fig.add_trace(go.Scatter(
x=[bw_nyq], y=[ref_br], mode="markers+text",
name=f"Reference: {bw_nyq:.1f} MHz, {ref_br:.1f} Mbps",
marker=dict(size=12, color="#FF7043", symbol="diamond"),
text=[f"{ref_br:.1f} Mbps"],
textposition="top center",
))
fig.update_layout(
title=f"Theoretical Bit Rate at Constant Power<br><sub>C/N₀ = {c_n0:.1f} MHz</sub>",
xaxis_title="Bandwidth [MHz]",
yaxis_title="Bit Rate [Mbps]",
template="plotly_dark",
height=500,
showlegend=True,
legend=dict(yanchor="bottom", y=0.02, xanchor="right", x=0.98),
)
return fig
def _make_power_sensitivity_plot(cnr_nyq: float, bw_nyq: float, cnr_linear: float) -> go.Figure:
"""Power sensitivity at constant bandwidth."""
n = 40
p_mul = np.zeros(n)
br = np.zeros(n)
cnr = np.zeros(n)
p_mul[0] = 1 / 8
cnr[0] = cnr_nyq - 10 * log(8, 10)
br[0] = shannon_capacity(bw_nyq, cnr[0])
for i in range(1, n):
p_mul[i] = p_mul[i - 1] * 2 ** (1 / 6)
cnr[i] = cnr[i - 1] + 10 * log(2 ** (1 / 6), 10)
br[i] = shannon_capacity(bw_nyq, cnr[i])
fig = go.Figure()
fig.add_trace(go.Scatter(
x=p_mul, y=br, mode="lines",
name="Shannon Capacity",
line=dict(color="#81C784", width=3),
))
# Reference point (multiplier = 1)
ref_br = shannon_capacity(bw_nyq, cnr_nyq)
fig.add_trace(go.Scatter(
x=[1.0], y=[ref_br], mode="markers+text",
name=f"Reference: 1x, {ref_br:.1f} Mbps",
marker=dict(size=12, color="#FF7043", symbol="diamond"),
text=[f"{ref_br:.1f} Mbps"],
textposition="top center",
))
fig.update_layout(
title=f"Theoretical Bit Rate at Constant Bandwidth: {bw_nyq:.1f} MHz<br>"
f"<sub>Reference: C/N = {cnr_linear:.1f} [Linear]</sub>",
xaxis_title="Power Multiplying Factor",
yaxis_title="Bit Rate [Mbps]",
template="plotly_dark",
height=500,
showlegend=True,
legend=dict(yanchor="bottom", y=0.02, xanchor="right", x=0.98),
)
return fig
def _make_br_factor_map(cnr_nyq: float, bw_nyq: float, c_n0: float, br_bw: float) -> go.Figure:
"""Contour map of BR multiplying factors."""
n = 41
bw_mul = np.zeros((n, n))
p_mul = np.zeros((n, n))
br_mul = np.zeros((n, n))
for i in range(n):
for j in range(n):
bw_mul[i, j] = (i + 1) / 8
p_mul[i, j] = (j + 1) / 8
br_mul[i, j] = br_multiplier(bw_mul[i, j], p_mul[i, j], cnr_nyq)
fig = go.Figure(data=go.Contour(
z=br_mul,
x=bw_mul[:, 0],
y=p_mul[0, :],
colorscale="Viridis",
contours=dict(showlabels=True, labelfont=dict(size=10, color="white")),
colorbar=dict(title="BR Factor"),
))
fig.update_layout(
title=f"Bit Rate Multiplying Factor<br><sub>Ref: C/N = {cnr_nyq:.1f} dB, "
f"BW = {bw_nyq:.1f} MHz, C/N₀ = {c_n0:.1f} MHz, "
f"BR = {br_bw:.1f} Mbps</sub>",
xaxis_title="Bandwidth Multiplying Factor",
yaxis_title="Power Multiplying Factor",
template="plotly_dark",
height=550,
)
return fig
def render():
"""Render the Theoretical Exploration page."""
# ── Header ──
col_img, col_title = st.columns([1, 3])
with col_img:
st.image("Shannon.png", width=200)
with col_title:
st.markdown("# 📡 Shannon's Equation for Dummies")
st.markdown(
"Exploration of Claude Shannon's channel capacity theorem — "
"the fundamental limit of digital communications."
)
st.link_button("📖 Wiki: Claude Shannon", "https://en.wikipedia.org/wiki/Claude_Shannon")
st.divider()
# ── Input Parameters ──
st.markdown("### ⚙️ Input Parameters")
col_in1, col_in2 = st.columns(2)
with col_in1:
cnr_input = st.text_input(
"Reference C/N [dB]",
value="12",
help=THEORY_HELP["cnr"],
)
with col_in2:
bw_input = st.number_input(
"Reference BW [MHz]",
value=36.0, min_value=0.1, step=1.0,
help=THEORY_HELP["bw"],
)
# Parse CNR (supports comma-separated combinations)
try:
cnr_values = [float(v.strip()) for v in cnr_input.split(",")]
cnr_nyq = combine_cnr(*cnr_values)
except (ValueError, ZeroDivisionError):
st.error("❌ Invalid C/N values. Use comma-separated numbers (e.g., '12' or '12, 15').")
return
# ── Computed Results ──
cnr_linear, br_inf, c_n0, br_bw = shannon_points(bw_input, cnr_nyq)
br_unit = c_n0 # Spectral efficiency = 1
st.markdown("### 📊 Results")
st.info(THEORY_HELP["c_n0"], icon="") if st.checkbox("Show C/N₀ explanation", value=False) else None
m1, m2, m3, m4 = st.columns(4)
m1.metric("C/N₀", f"{c_n0:.1f} MHz", help=THEORY_HELP["c_n0"])
m2.metric("BR at ∞ BW", fmt_br(br_inf), help=THEORY_HELP["br_inf"])
m3.metric("BR at SpEff=1", fmt_br(br_unit), help=THEORY_HELP["br_unit"])
m4.metric("BR at Ref BW", fmt_br(br_bw), help=THEORY_HELP["br_bw"])
st.metric(
"C/N Ratio",
f"{cnr_nyq:.1f} dB · {cnr_linear:.1f} linear",
help=THEORY_HELP["cnr_lin"],
)
st.divider()
# ── Sensitivity Analysis ──
st.markdown("### 🔬 Sensitivity Analysis")
col_s1, col_s2, col_s3 = st.columns(3)
with col_s1:
bw_mul_val = st.number_input(
"BW Increase Factor",
value=1.0, min_value=0.01, step=0.25,
help=THEORY_HELP["bw_mul"],
)
with col_s2:
p_mul_val = st.number_input(
"Power Increase Factor",
value=2.0, min_value=0.01, step=0.25,
help=THEORY_HELP["p_mul"],
)
with col_s3:
br_mul_val = br_multiplier(bw_mul_val, p_mul_val, cnr_nyq)
st.metric(
"Bit Rate Factor",
f"{br_mul_val:.3f}",
delta=f"{(br_mul_val - 1) * 100:+.1f}%",
help=THEORY_HELP["br_mul"],
)
st.divider()
# ── Graphs ──
st.markdown("### 📈 Interactive Graphs")
tab_bw, tab_pow, tab_map = st.tabs([
"📶 Bandwidth Sensitivity",
"⚡ Power Sensitivity",
"🗺️ BR Factor Map",
])
with tab_bw:
st.plotly_chart(
_make_bw_sensitivity_plot(cnr_nyq, bw_input, c_n0),
width="stretch",
)
with tab_pow:
st.plotly_chart(
_make_power_sensitivity_plot(cnr_nyq, bw_input, cnr_linear),
width="stretch",
)
with tab_map:
st.plotly_chart(
_make_br_factor_map(cnr_nyq, bw_input, c_n0, br_bw),
width="stretch",
)
# ── Help Section ──
with st.expander("📘 Background Information"):
help_topic = st.selectbox(
"Choose a topic:",
options=["shannon", "advanced", "help"],
format_func=lambda x: {
"shannon": "🧠 Shannon's Equation",
"advanced": "🔧 Advanced (AWGN Model)",
"help": "❓ How to use this tool",
}[x],
)
st.markdown(THEORY_HELP[help_topic])