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
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:
0
views/__init__.py
Normal file
0
views/__init__.py
Normal file
BIN
views/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
views/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/contributions.cpython-313.pyc
Normal file
BIN
views/__pycache__/contributions.cpython-313.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/orbits_animation.cpython-313.pyc
Normal file
BIN
views/__pycache__/orbits_animation.cpython-313.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/real_world.cpython-313.pyc
Normal file
BIN
views/__pycache__/real_world.cpython-313.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/satellite_animation.cpython-313.pyc
Normal file
BIN
views/__pycache__/satellite_animation.cpython-313.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/satellite_types.cpython-313.pyc
Normal file
BIN
views/__pycache__/satellite_types.cpython-313.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/theory.cpython-313.pyc
Normal file
BIN
views/__pycache__/theory.cpython-313.pyc
Normal file
Binary file not shown.
130
views/contributions.py
Normal file
130
views/contributions.py
Normal 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
585
views/orbits_animation.py
Normal 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
412
views/real_world.py
Normal 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])
|
||||
762
views/satellite_animation.py
Normal file
762
views/satellite_animation.py
Normal 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 10–20 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
917
views/satellite_types.py
Normal 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.2–1.6 GHz)',power:'~50 W/signal',
|
||||
precision:'< 1 m (dual-freq)',lifetime:'12–15 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:'550–36 000 km',band:'C / Ku / Ka-band',power:'2–20 kW',
|
||||
precision:'100+ Gbps (HTS)',lifetime:'15–20 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:'500–800 km',band:'X-band (downlink)',power:'SAR + optical',
|
||||
precision:'0.3–30 m resolution',lifetime:'5–7 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:'800–36 000 km',band:'L / S / Ka-band',power:'Imager + Sounder',
|
||||
precision:'Full disk every 10 min',lifetime:'10–15 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:'5–20+ 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:'7–15 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 10–15 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
269
views/theory.py
Normal 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])
|
||||
Reference in New Issue
Block a user