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:
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
.git
|
||||
.gitignore
|
||||
*.save
|
||||
*.db
|
||||
.venv
|
||||
venv
|
||||
README.md
|
||||
136
.gitea/workflows/deploy-shannon.yml
Normal file
136
.gitea/workflows/deploy-shannon.yml
Normal file
@@ -0,0 +1,136 @@
|
||||
name: Build & Deploy Shannon
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'app.py'
|
||||
- 'core/**'
|
||||
- 'views/**'
|
||||
- 'requirements.txt'
|
||||
- 'Dockerfile'
|
||||
- 'docker-stack.yml'
|
||||
- '.streamlit/**'
|
||||
- '.gitea/workflows/deploy-shannon.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
IMAGE_NAME: shannon/streamlit
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
name: 🏗️ Build & Deploy Shannon
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 📥 Checkout code
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: 🔐 Setup SSH
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SWARM_SSH_KEY }}
|
||||
SSH_HOST: ${{ secrets.SWARM_MANAGER_HOST }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
|
||||
|
||||
- name: 📦 Copy source to server
|
||||
env:
|
||||
SSH_USER: ${{ secrets.SWARM_USER }}
|
||||
SSH_HOST: ${{ secrets.SWARM_MANAGER_HOST }}
|
||||
run: |
|
||||
tar -czf /tmp/shannon-source.tar.gz \
|
||||
--exclude='.git' \
|
||||
--exclude='.venv' \
|
||||
--exclude='__pycache__' \
|
||||
--exclude='*.db' \
|
||||
--exclude='*.pyc' \
|
||||
-C . .
|
||||
scp -i ~/.ssh/deploy_key /tmp/shannon-source.tar.gz "$SSH_USER@$SSH_HOST":/tmp/
|
||||
echo "✅ Source copied"
|
||||
|
||||
- name: 🐳 Build Docker Image
|
||||
env:
|
||||
SSH_USER: ${{ secrets.SWARM_USER }}
|
||||
SSH_HOST: ${{ secrets.SWARM_MANAGER_HOST }}
|
||||
run: |
|
||||
COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||
echo "📌 Commit: $COMMIT_SHA"
|
||||
|
||||
ssh -i ~/.ssh/deploy_key ${SSH_USER}@${SSH_HOST} << ENDSSH
|
||||
set -e
|
||||
cd /tmp
|
||||
rm -rf shannon-build
|
||||
mkdir -p shannon-build
|
||||
cd shannon-build
|
||||
tar -xzf /tmp/shannon-source.tar.gz
|
||||
|
||||
echo "🐳 Building image with tag: $COMMIT_SHA"
|
||||
docker build -t shannon/streamlit:$COMMIT_SHA \
|
||||
-t shannon/streamlit:latest .
|
||||
|
||||
echo "🧹 Cleaning old images..."
|
||||
docker image prune -f
|
||||
|
||||
echo "✅ Image built: $COMMIT_SHA"
|
||||
ENDSSH
|
||||
|
||||
- name: 🚀 Deploy to Swarm
|
||||
env:
|
||||
SSH_USER: ${{ secrets.SWARM_USER }}
|
||||
SSH_HOST: ${{ secrets.SWARM_MANAGER_HOST }}
|
||||
run: |
|
||||
COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||
|
||||
scp -i ~/.ssh/deploy_key ./docker-stack.yml ${SSH_USER}@${SSH_HOST}:/tmp/shannon-docker-stack.yml
|
||||
|
||||
ssh -i ~/.ssh/deploy_key ${SSH_USER}@${SSH_HOST} << ENDSSH
|
||||
set -e
|
||||
|
||||
echo "🚀 Deploying stack..."
|
||||
docker stack deploy -c /tmp/shannon-docker-stack.yml shannon
|
||||
|
||||
echo "🔄 Forcing service update to use new image..."
|
||||
docker service update --force --image shannon/streamlit:$COMMIT_SHA shannon_shannon
|
||||
|
||||
echo "⏳ Waiting for service to update..."
|
||||
sleep 15
|
||||
|
||||
echo "📊 Service status:"
|
||||
docker service ls --filter name=shannon_shannon
|
||||
docker service ps shannon_shannon --no-trunc
|
||||
|
||||
echo "✅ Deployment complete!"
|
||||
ENDSSH
|
||||
|
||||
- name: 🏥 Health Check
|
||||
env:
|
||||
SSH_USER: ${{ secrets.SWARM_USER }}
|
||||
SSH_HOST: ${{ secrets.SWARM_MANAGER_HOST }}
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key ${SSH_USER}@${SSH_HOST} << ENDSSH
|
||||
echo "🏥 Running health check..."
|
||||
|
||||
for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
||||
HTTP_CODE=\$(curl -s -o /dev/null -w "%{http_code}" https://shannon.antopoid.com/_stcore/health)
|
||||
if [ "\$HTTP_CODE" = "200" ]; then
|
||||
echo "✅ Shannon is accessible (HTTP 200)!"
|
||||
exit 0
|
||||
fi
|
||||
echo "⏳ Waiting for Shannon... (\$i/12) - Status: \$HTTP_CODE"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "❌ Health check failed!"
|
||||
echo "📋 Service logs:"
|
||||
docker service logs shannon_shannon --tail 50
|
||||
exit 1
|
||||
ENDSSH
|
||||
|
||||
- name: 🧹 Cleanup
|
||||
run: |
|
||||
rm -f ~/.ssh/deploy_key
|
||||
echo "✅ Deployment finished!"
|
||||
21
.streamlit/config.toml
Normal file
21
.streamlit/config.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[server]
|
||||
port = 8080
|
||||
address = "0.0.0.0"
|
||||
headless = true
|
||||
enableCORS = false
|
||||
enableXsrfProtection = false
|
||||
maxUploadSize = 5
|
||||
runOnSave = false
|
||||
|
||||
[browser]
|
||||
gatherUsageStats = false
|
||||
|
||||
[theme]
|
||||
primaryColor = "#4FC3F7"
|
||||
backgroundColor = "#0E1117"
|
||||
secondaryBackgroundColor = "#1a1a2e"
|
||||
textColor = "#E2E8F0"
|
||||
font = "sans serif"
|
||||
|
||||
[logger]
|
||||
level = "info"
|
||||
35
Dockerfile
35
Dockerfile
@@ -1,11 +1,36 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies (gcc for build, curl for healthcheck)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt ./
|
||||
RUN apt-get update -y && apt-get install -y tk tcl
|
||||
RUN pip install --force-reinstall -r requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
# Copy application
|
||||
COPY .streamlit/ .streamlit/
|
||||
COPY core/ core/
|
||||
COPY views/ views/
|
||||
COPY app.py .
|
||||
COPY Shannon.png .
|
||||
COPY Satellite.png .
|
||||
|
||||
CMD [ "python", "./Shannon.py" ]
|
||||
# Create data directory for SQLite databases
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# Expose Streamlit port
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/_stcore/health || exit 1
|
||||
|
||||
# Run Streamlit
|
||||
ENTRYPOINT ["streamlit", "run", "app.py", \
|
||||
"--server.port=8080", \
|
||||
"--server.address=0.0.0.0", \
|
||||
"--server.headless=true"]
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY requirements.txt ./
|
||||
echo "http://nova.clouds.archive.ubuntu.com/ubuntu jammy/main amd64 Packages" >
|
||||
RUN apt install tk/jammy
|
||||
RUN pip install --force-reinstall -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD [ "python", "./Shannon.py" ]
|
||||
8199
PySimpleGUIWeb.py
8199
PySimpleGUIWeb.py
File diff suppressed because it is too large
Load Diff
113
README.md
113
README.md
@@ -1,18 +1,111 @@
|
||||
# Shannon-for-Dummies
|
||||
# Shannon's Equation for Dummies
|
||||
|
||||
Educational application
|
||||
**Educational Web Application for Satellite Communications**
|
||||
|
||||
Exploration from Claude's Shannon initial theory to its practical application to satellite communications.
|
||||
Exploration from Claude Shannon's initial theory to its practical application to satellite communications.
|
||||
|
||||
The application is using PysimpleGUI / PySimpleGUIWeb and runs either in local windows or in web pages.
|
||||
## 🚀 Stack
|
||||
|
||||
The Web version via localhost is fully functional although not as convenient as the windowed version (only 1 plot open).
|
||||
The look of the web version differs significantly from the local version (PySimpleGUIWeb is still beta).
|
||||
- **Frontend**: [Streamlit](https://streamlit.io) with [Plotly](https://plotly.com) interactive charts
|
||||
- **Backend**: Python 3.11+ with scientific libraries (numpy, scipy, itur, astropy)
|
||||
- **Database**: SQLite for community contributions
|
||||
- **Deployment**: Docker + docker-compose, designed for 24/7 multi-user operation
|
||||
|
||||
The Web version in remote does work for a single user (all users connected can send commands but see the same page).
|
||||
This mode is experimental and doesn't behave as a web server : users are not managed, the app closes when the user closes the window ...
|
||||
## 📁 Project Structure
|
||||
|
||||
The value of the application is essentially in the background information accessed by clicking labels of all inputs / outputs.
|
||||
```
|
||||
.
|
||||
├── app.py # Main Streamlit entry point
|
||||
├── core/ # Core business logic
|
||||
│ ├── calculations.py # Shannon equations & satellite link budget
|
||||
│ ├── database.py # SQLite contribution management
|
||||
│ └── help_texts.py # Educational help content
|
||||
├── views/ # UI pages
|
||||
│ ├── theory.py # Theoretical exploration (Shannon limit)
|
||||
│ ├── real_world.py # Real-world link budget calculator
|
||||
│ └── contributions.py # Community knowledge database
|
||||
├── .streamlit/config.toml # Streamlit configuration
|
||||
├── Dockerfile # Container image definition
|
||||
├── docker-compose.yml # Orchestration with nginx-proxy
|
||||
└── requirements.txt # Python dependencies
|
||||
```
|
||||
|
||||
A Knowledge DB is coupled to the application for collaborative contributions on the subject opening technical discussions. At this stage the DB is local.
|
||||
## 🛠️ Local Development
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.11+
|
||||
- pip
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Create virtual environment
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run the app
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
The app will open at `http://localhost:8501`
|
||||
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
### Build and run
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
The app will be available at `http://localhost:8080`
|
||||
|
||||
### Environment
|
||||
|
||||
- **Port**: 8080 (configurable in `.streamlit/config.toml`)
|
||||
- **Health Check**: `/_stcore/health`
|
||||
- **Data Persistence**: SQLite databases stored in `shannon_data` volume
|
||||
|
||||
### Production Setup
|
||||
|
||||
The `docker-compose.yml` is configured for nginx-proxy:
|
||||
- Network: `nginx-proxy`
|
||||
- Virtual host: `shannon.antopoid.com`
|
||||
- HTTPS ready (with appropriate Let's Encrypt configuration)
|
||||
|
||||
## 📚 Features
|
||||
|
||||
### 1. Theoretical Exploration
|
||||
- Shannon capacity calculation (C = BW × log₂(1 + C/N))
|
||||
- Bandwidth and power sensitivity analysis
|
||||
- Bit rate factor maps
|
||||
- Interactive Plotly graphs
|
||||
|
||||
### 2. Real-World Link Budget
|
||||
- Complete satellite link calculation
|
||||
- ITU-R atmospheric attenuation models
|
||||
- Receiver noise and baseband impairments
|
||||
- Practical Shannon limits with penalties
|
||||
|
||||
### 3. Community Contributions
|
||||
- Collaborative knowledge database
|
||||
- Search and filter capabilities
|
||||
- Read/Write/Delete permissions
|
||||
|
||||
## 🎓 Educational Value
|
||||
|
||||
The application provides extensive background information accessible through help expanders on every input/output, covering:
|
||||
- Shannon's theorem and its implications
|
||||
- Satellite communication fundamentals
|
||||
- Atmospheric propagation effects
|
||||
- Modulation and coding schemes
|
||||
- Real-world system design trade-offs
|
||||
|
||||
## 📝 License & Author
|
||||
|
||||
© 2021-2026 · Shannon Equation for Dummies
|
||||
Created by JPC (February 2021)
|
||||
Migrated to Streamlit (2026)
|
||||
|
||||
|
||||
1028
Shannon.py
1028
Shannon.py
File diff suppressed because it is too large
Load Diff
1024
Shannon.py.save
1024
Shannon.py.save
File diff suppressed because it is too large
Load Diff
330
Shannon_Dict.py
330
Shannon_Dict.py
@@ -1,330 +0,0 @@
|
||||
|
||||
Help={'-iCNR-':'Reference C/N [dB]\n\nReference Carrier to Noise Ratio in decibels : 10 log (C/N), where C is the '
|
||||
'Carrier\'s power and N is the Noise\'s Power, both are measured in the reference '
|
||||
'Channel Bandwidth.\n\nThe Carrier\'s power if often named Signal\'s Power and the Carrier to Noise Ratio'
|
||||
' is often named Signal to Noise Ratio.',
|
||||
'-iBW-':'Reference BW [MHz]\n\nReference Channel Bandwidth, this is a key parameter of the communication channel.'
|
||||
'\n\nThis bandwidth is usually a degree of freedom of the system design, eventually constrained by technological'
|
||||
' constraints and various kind of frequency usage regulations.',
|
||||
'-iC_N0-':'Carrier Power to Noise Power Density Ratio : C/N\N{SUBSCRIPT ZERO}\n\nCarrier\'s power (in Watts) '
|
||||
'divided by the Noise Spectral Power Density (in Watts per MHz), the result\'s units are MHz.',
|
||||
'-iBRinf-':'Theoretical BR at infinite BW : 1.44 C/N\N{SUBSCRIPT ZERO}\n\nBit Rate theoretically achievable when '
|
||||
'the signal occupies an infinite Bandwidth, this value is a useful asympotical limit.',
|
||||
'-iBRunit-':'Theoretical BR at Spectral Efficiency = 1 : C/N\N{SUBSCRIPT ZERO}\n\nBit Rate theoretically '
|
||||
'achievable at a Spectral Efficiency = 1 : Bit Rate in Mbps = Bandwith in MHz.'
|
||||
'\n\nThe corresponding value, deduced from the Shannon\'s formula is given by C/N\N{SUBSCRIPT ZERO}.',
|
||||
'-iBRbw-':'Theoretical BR at Reference (BW,C/N)\n\nBit Rate theoretically achievable when the Bandwidth is '
|
||||
'constrained to the given reference value.',
|
||||
'-iCNRlin-':'C / N = C / (N\N{SUBSCRIPT ZERO}.B)\n\nReference Carrier to Noise Ratio (or Signal to Noise Ratio).'
|
||||
' The C/N Ratio or CNR is usually given in dBs or 10 log (C/N). '
|
||||
'\n\nAlthough the logarithm is convenient for many evaluations (multiplications become additions), '
|
||||
'it\'s also good to consider the ratio itself (named here Linear '
|
||||
'Format) to get some physical sense of the power ratio.'
|
||||
'\n\nThe Carrier to Noise Ratio in linear format, is the value used in the Shannon\'s formula.',
|
||||
'-iBRmul-':'Bit Rate Increase Factor\n\nBit Rate multiplying factor achieved when the Bandwidth and the Power '
|
||||
'and multiplied by a given set of values.',
|
||||
'-iCmul-':'Power Increase Factor\n\nArbitrary multiplying factor applied to the Carrier\'s Power, for '
|
||||
'sensitivity analysis.',
|
||||
'-iBWmul-':'BW Increase Factor\n\nArbitrary multiplying factor applied to the Carrier\'s Bandwidth, for '
|
||||
'sensitivity analysis.',
|
||||
'-CRC-':'Cyclic Redundancy Check of the input parameters, for changes identification purposes.\n\n'
|
||||
'https://en.wikipedia.org/wiki/Cyclic_redundancy_check',
|
||||
'Advanced': 'The model assumes that the communication channel is \"AWGN\", just Adding White Gaussian Noise to '
|
||||
'the signal. This noise is supposed to be random and white which means that noise at a given time is '
|
||||
'independent of noise at any other time, this implies that the noise has a flat and infinite spectrum.'
|
||||
'This noise is also supposed to be Gaussian which means that its probability density function follows a '
|
||||
'Gaussian law, with a variance associated to the Noise\'s power.\n\n'
|
||||
'Although these assumptions seem very strong, they are quite accurately matching the cases of interest. '
|
||||
'Many impairments are actually non linear and/or non additive, but just combining equivalent C/N of all '
|
||||
'impairments as if they were fully AWGN is in most of the cases very accurate.'
|
||||
'The reason for that is that the sum of random variables of unknown laws always tend to Gaussian and that '
|
||||
'in most systems, thermal noise is dominating, is actually white and gaussian and is whitening the rest.\n\n'
|
||||
'The tool accepts lists of comma separated CNRs which will be combined in that way. \n\n'
|
||||
'In satellite systems, the noise is mainly coming from the electronics of the radio front end, the ground '
|
||||
'seen by the antenna, the stars and the atmospheric attenuator. In case of rain, the signal is punished twice '
|
||||
': the attenuation makes it weaker and the rain attenuator generates noise added to the overall noise.\n\n'
|
||||
'Overall the Shannon Limit is a pretty convenient tool to predict the real performances of communication '
|
||||
'systems and even more importantly to get a sense of the role of the key design parameters.',
|
||||
'-iShannon-':'The Shannon Limit allows to evaluate the theoretical capacity achievable over a communication '
|
||||
'channel.\n\nAs a true genius, Claude Shannon has funded the communication theory, the information theory '
|
||||
'and more (click the Wikipedia button for more info).\n\nThis equation is fundamental for the evaluation of '
|
||||
'communication systems. It is an apparently simple but extremely powerful tool to guide communication systems\''
|
||||
' designs.\n\nThis equation tells us what is achievable, not how to achieve it. It took almost 50 years to '
|
||||
'approach this limit with the invention of Turbo codes.\n\nIn the satellite domain, DVB-S2x, using LDPC codes '
|
||||
'iteratively decoded (Turbo-Like), is only 1 dB away from this limit.',
|
||||
'Help': 'Recommendations for using the tool\n\nThe first purpose of the tool is educational, allowing people to '
|
||||
'better understand the physics of communications and the role of key parameters.\n\n'
|
||||
'The user should try multiple values in all the fields one per one, explore the graphs and try to '
|
||||
'understand the underlying physics.\n\n'
|
||||
'The units for the different figures are as explicit as possible to facilitate the exploration.\n\n'
|
||||
'All labels can be \"clicked\" to get information about associated item. All values (including this text)'
|
||||
' can be copy/pasted for further usage.'
|
||||
}
|
||||
|
||||
Help2={'-iFreq-':'Frequency [GHz]\n\nFrequency of the electromagnetic wave supporting the communication in GHz '
|
||||
'(billions of cycles per second).\n\nFor satellite downlink (satellite to terminal), frequency bands '
|
||||
'and frequencies are typically : L : 1.5 GHz , S : 2.2 GHz , C : 4 GHz , Ku : 12 GHz, Ka : 19 GHz, '
|
||||
'Q : 40 GHz',
|
||||
'-iSatAlt-':'Satellite Altitude [km]\n\nThe position of the satellite is expressed in latitude, longitude, '
|
||||
'altitude. The program doesnt simulate the orbit, any satellite coordinates can be used. '
|
||||
'A GEO satellite has a latitude of zero degrees and an altitude of 35786 km. LEO satellites '
|
||||
'have an altitude lower than 2000 km. MEO\'s altitudes are between LEO and GEO : the O3B '
|
||||
'constellation\'s altitude is 8063 km',
|
||||
'-iSatLatLong-':'Satellite Latitude and Longitude [\N{DEGREE SIGN}]\n\nThe position of the satellite is '
|
||||
'expressed in latitude, longitude, altitude. The program doesnt simulate the orbit, any '
|
||||
'satellite coordinates can be used. A GEO satellite has a latitude of zero degrees and an '
|
||||
'altitude of 35786 km. LEO satellites have an altitude lower than 2000 km. MEO\'s altitudes are'
|
||||
' between LEO and GEO : the O3B constellation\'s altitude is 8063 km',
|
||||
'-iGSLatLong-':'Ground Station Latitude and Longitude [\N{DEGREE SIGN}]\n\nThe position of the ground station '
|
||||
'is expressed in latitude, longitude (the ground station is assumed to be at the surface of '
|
||||
'the earth).\n\nThe position of the ground station is affecting the link availability due to'
|
||||
' the differences in weather statistics at different locations on earth (tropical regions have '
|
||||
'very heavy rains attenuating dramatically signals at high frequencies). It is also impacting '
|
||||
'the elevation angle at which the satellite is seen and thus the length of the path in the rain.'
|
||||
'\n\nThe position of the ground station is also impacting the overall path length and thus the '
|
||||
'path dispersion loss.\n\nUseful link to find coordinates of interest : '
|
||||
'https://www.gps-coordinates.net',
|
||||
'-iAvail-':'Desired Link Availability [%]\n\nThe link availability in percentage of the time is a key '
|
||||
'performance indicator for satellite communications.\n\nIn this program the only cause of '
|
||||
'unavailability modelled in a probabilistic way is the attenuation caused by the atmosphere. A high '
|
||||
'desired link availability corresponds to a high signal attenuation : only rare and severe weather '
|
||||
'events exceeding this attenuation can interrupt the link.\n\nFor example for an availability of'
|
||||
'99.9%, the attenuation considered in the link sizing is only exceeded for 0.1% of the time.',
|
||||
'-iPathLength-':'Path Length [km] @ Elevation [\N{DEGREE SIGN}]\n\nDistance in kilometers from the satellite '
|
||||
'to the ground station and elevation angle at which the satellite is seen. The actual distance'
|
||||
' depends on the satellite\'s altitude and on the relative positions of the satellite and the '
|
||||
'ground station.\n\nThe minimum path length is the satellite altitude, achieved when the ground'
|
||||
' station is under the satellite (elevation = 90\N{DEGREE SIGN}).\n\nA negative elevation '
|
||||
'implies that the satellite is not visible (beyond the horizon).',
|
||||
'-iAtmLoss-':'Overall Atmospheric Attenuation [dB]\n\nThe Atmosphere is affecting radio wave propagation '
|
||||
'with a signal attenuation caused by rain precipitations and clouds, by scintillation and '
|
||||
'multi path effects, by sand and dust storms and also by atmospheric gases. \n\n'
|
||||
'Simply speaking, the attenuation is increasing with the rain intensity and with the signal '
|
||||
'frequency. C band is almost unaffected, Ku band is significantly affected, Ka band is severely '
|
||||
'affected, Q band is dramatically affected\n\nThe overall attenuation depends on the actual '
|
||||
'geographical location and on actual weather events. By nature, it is is thus statistical '
|
||||
'(considering the past) or probabilistic (considering the future).\n\nAll effects included, '
|
||||
'here are typical attenuation figures exceeded for 0.1% of the time in Europe from the GEO orbit '
|
||||
': Ku: 2.5 dB, Ka: 6.9 dB, 22 dB \n\n'
|
||||
'The program uses ITU-Rpy, python implementation of the ITU-R P Recommendations: '
|
||||
'https://itu-rpy.readthedocs.io/en/latest/index.html',
|
||||
'-iHPA-':'HPA Power at operating point [W]\n\nPower of the High Power Amplifier used as a last stage of '
|
||||
'amplification in the satellite payload.'
|
||||
'The value in watts is the value at operating point and for the carrier of interest.\n\n'
|
||||
'Some satellites operate their HPAs at saturation in single carrier mode (typical DTH case).'
|
||||
'Other satellites operate in multicarrier mode and reduced power (3dB Output Back Off is typical '
|
||||
'for satellites serving VSATs)',
|
||||
'-iSBeam-':'Satellite Half Power Beam Diameter [\N{DEGREE SIGN}]\n\nBeam diameter expressed as an angle at '
|
||||
'satellite level. The power radiated at the edge of this beam is half of the power radiated at '
|
||||
'the peak of the beam (on-axis value).\n\n'
|
||||
'The beam evaluated is a basic one with simple illumination of a parabolic reflector\n\n'
|
||||
'Typical beam size : 0.4-1.4 degrees for GEO HTS satellites, 3..6 degrees for GEO DTH satellites.',
|
||||
'-iGOff-': 'Gain Offset from Peak [dB]\n\nThis offset allows to simulate terminals which are not all at '
|
||||
'the beam peak. A 3 dB value would simulate a worst case position in a 3dB beam, typical approach '
|
||||
'used in DTH. In single feed per beam HTS, a 1 dB value would give a typical median performance.'
|
||||
'If you know the EIRP you have, the best is to iterate this value to get this EIRP '
|
||||
'(the process will allow you to get a feeling of the tradeoff power / footprint size / EIRP. ',
|
||||
'-iLoss-':'Output Section Losses [dB]\n\nLoss of signal\'s power in the path connecting the HPA to the '
|
||||
'antenna. This loss is associated with filters, waveguide sections, switches ...\n\n'
|
||||
'Typical value : 2.5 dB for large classical satellites, 1 dB for active antennas with HPAs close to '
|
||||
'the feeds. If the power value is given at antenna level, the value should just be set to zero.',
|
||||
'-iSCIR-':'Satellite C/I [dB]\n\nEffect of signal impairments associated with satellite implementation,'
|
||||
'expressed as a signal to impairment noise ratio to be combined with the intrinsic Signal to Noise '
|
||||
'Ratio affecting the link. Typical impairments are : intermodulation in the HPA, filtering effects, '
|
||||
'oscillator\'s phase noise ...\n\n'
|
||||
'The tool supports comma separated lists of C/I or C/N values expressed in dB. In addition to '
|
||||
'satellites impairments, one can use this feature to also simulate infrastructure C/N, uplink C/N, '
|
||||
'uplink interferences ...',
|
||||
'-iOPow-':'Output Power [W]\n\nThe output power in watts at antenna output is associated with the useful '
|
||||
'signal carrying user\'s information. It is also common to express this value in dBs (dBs transform '
|
||||
'multiplications in additions, easier for human computation. Nevertheless, reasoning in watts tells '
|
||||
'more about the physics.',
|
||||
'-iSGain-':'Satellite Antenna Gain \n\nAn antenna concentrating the signal in the direction of the users is '
|
||||
'almost always required to compensate for the path loss associated with the distance from the '
|
||||
'satellite to the terminal.\n\nThe antenna gain is the ratio between the signal radiated '
|
||||
'on the axis of the antenna (direction of maximum radiation) and the signal radiated by an '
|
||||
'antenna radiating equally in all directions (for the same input power).\n\n'
|
||||
'Antenna gains are without units but can be expressed in dB for convenience : dBi = dB relative to'
|
||||
' isotropic antenna (antenna radiating equally in all directions)',
|
||||
'-iEIRP-':'Equivalent Isotropic Radiated Power\n\nThe product Power x Gain expressed in Watts is a convenient '
|
||||
'characterisation of the satellite radiation capability. It does correspond to the power which would '
|
||||
'be required for an isotropic antenna radiating in the same way in the direction of the antenna '
|
||||
'considered.\n\nThere is no "power creation" of course : for the directive antenna, the integral of '
|
||||
'the radiated signal over a sphere centered on the antenna is at best equal to the input power '
|
||||
'(lossless antenna).\n\n'
|
||||
'As the value in watts is usually pretty big, a value in dBW is more convenient '
|
||||
'for practical human computations.',
|
||||
'-iPLoss-':'Path Dispersion Loss\n\nAssuming communication in free space (thus also in the vacuum), '
|
||||
'this figure characterises the effect'
|
||||
' of the distance from the satellite to the terminal. It gives an attenuation equivalent to the '
|
||||
'inverse ratio of the power reaching one square meter at the terminal side and the equivalent '
|
||||
'isotropic radiated power at satellite level.\n\n'
|
||||
'This simply equals the surface in square meters of a sphere with a radius equal to the path length.'
|
||||
'This attenuation is pretty big and is thus more humanly manageable in dB m\N{SUPERSCRIPT TWO}.\n\n'
|
||||
'As the the vacuum is lossless, this "attenuation" is simply associated with the fact that only '
|
||||
'a marginal fraction of the power radiated is captured in one square meter at destination, '
|
||||
'the rest is going somewhere else.',
|
||||
'-iPFD-':'Power Flux Density\n\nSignal power per square meter at the terminal side. '
|
||||
'The actual power captured by the terminal is given by this value multiplied by the effective surface '
|
||||
'of the terminal\'s antenna.\n\nNote that if the surface of antenna is not perpendicular to the '
|
||||
'propagation direction of the radio wave, the effective surface presented to the wave is reduced '
|
||||
'and less power is captured.',
|
||||
'-iCPE-':'Customer Antenna Size [m]\n\nSize of the terminal antenna. A basic parabolic antenna with state of '
|
||||
'the art efficiency is assumed.\n\n'
|
||||
'The main source of noise is in general the terminal\'s radio front end'
|
||||
' attached to the antenna. A state of the art Noise Temperature of 80K is assumed for this front end.',
|
||||
'-iCPE_T-':'Noise Temperature [K]\n\nTotal Receiver\'s Clear Sky Noise Temperature. It includes all noise '
|
||||
'temperature\'s contributors : receiver, sky, ground seen by the antenna... Antenna catalogs often '
|
||||
'provide this value, the proposed default of 120K is a reasonable typical value. The computation '
|
||||
'under rain fade conditions assumes 40K is affected by rain attenuation and the rest is not. ',
|
||||
'-iCGain-':'Customer Antenna Effective Area and G/T\n\nThe effective area in square meters is expressing the '
|
||||
'capability of the terminal to capture the Power Flux Density '
|
||||
'(the multiplication of both give the power captured). The effective area is typically 60% of the '
|
||||
'physical surface of the antenna\'s aperture.'
|
||||
'This capability can also be equivalently expressed as a gain as it\'s the case for the satellite '
|
||||
'antenna.\n\nThe figure of merit of a receive antenna is best expressed as the G/T ratio, '
|
||||
'ratio between antenna gain and the total Noise temperature in Kelvins. The noise is mainly coming '
|
||||
'from the electronics of the radio front end, the ground seen by the antenna, the stars and the '
|
||||
'atmospheric attenuator.\n\nIn case of rain, the signal is punished twice : the '
|
||||
'attenuation makes it weaker and the rain attenuator generates noise added to the overall noise.\n\n'
|
||||
'The noise power density N\N{SUBSCRIPT ZERO} is derived from the noise temperature with a very '
|
||||
'simple formula : N\N{SUBSCRIPT ZERO}=kTB (k being the Boltzmann constant), '
|
||||
'the G/T leads easily to the key overall link figure of merit C/N\N{SUBSCRIPT ZERO}.',
|
||||
'-iRXPow-':'RX Power at Antenna Output\n\nPower at receiver\'s antenna output before amplification. '
|
||||
'This power is extremely small and can only be exploited after strong amplification.\n\n'
|
||||
'As the main source of noise is in general coming from this amplification, the first amplification '
|
||||
'stage has to be a Low Noise Amplifier.\n\nThis power is "C" in the Shannon\'s equation.',
|
||||
'-iN0-' : 'Noise Power Density Antenna Output\n\nNoise Spectral Power Density of the radio front end under '
|
||||
'actual link conditions (in Watts per MHz). '
|
||||
'This PSD is N\N{SUBSCRIPT ZERO} in the Shannon\'s equation',
|
||||
'-iBRinf-':'Bit Rate at infinite Bandwidth\n\nBit Rate theoretically achievable when the signal occupies an '
|
||||
'infinite Bandwidth, this value is a useful asymptotic limit. The corresponding value, deduced '
|
||||
'from the Shannon\'s formula is given by 1.443 C/N\N{SUBSCRIPT ZERO}\n\nThis bit rate is an '
|
||||
'asymptotic value and is thus never achieved in practice.',
|
||||
'-iBRhalf-':'Bit Rate at Spectral Efficiency=1/2\n\nBit Rate theoretically achievable at a Spectral Efficiency '
|
||||
'= 1/2. The corresponding value, deduced from the Shannon\'s formula is given by 1.207 '
|
||||
'C/N\N{SUBSCRIPT ZERO}\n\nThis operating point is bandwidth intensive ( bandwidth = 2 x bit rate). '
|
||||
'Practical systems allow this operating point ( DVB-S2\'s QPSK 1/4 )',
|
||||
'-iBRUnit-':'Bit Rate at Spectral Efficiency=1\n\nBit Rate theoretically achievable at a Spectral Efficiency '
|
||||
'= 1. The corresponding value, deduced from the Shannon\'s formula is given by '
|
||||
'C/N\N{SUBSCRIPT ZERO}.\n\nThis data point has remarkable attributes : bandwidth = bit rate and '
|
||||
'C/N = 1 (equivalent to 0 dB), which means Noise Power = Signal Power.',
|
||||
'-iBRdouble-':'Bit Rate at Spectral Efficiency=2\n\nBit Rate theoretically achievable at a Spectral Efficiency '
|
||||
'= 1. The corresponding value, deduced from the Shannon\'s formula is given by '
|
||||
'0.667 C/N\N{SUBSCRIPT ZERO}.\n\nThis operating point is relatively bandwidth efficient '
|
||||
'( bandwidth = 0.5 x bit rate) and is often considered as a typical setting.',
|
||||
'-iBW-':'Available Bandwidth [MHz]\n\nBandwith occupied by the communication channel. This bandwidth is usually'
|
||||
' a degree of freedom of the system design, eventually constrained by technological constraints and '
|
||||
'various kind of frequency usage regulations. Interestingly this parameter is also often mentally '
|
||||
'constrained by past usages which were driven by technological constraints at that time.',
|
||||
'-iRO-':'Nyquist Filter Rolloff [%]\n\n'
|
||||
'To pass a limited bandwidth channel symbol have to be mapped on pulses, "filtered" to limit the '
|
||||
'Bandwidth occupied. Theoretically, filtering can be "brickwall", one symbol per second passing in '
|
||||
'1 Hertz. Practically, an excess of bandwidth is required, also called "Roll-Off of the filter.\n\n'
|
||||
'The filter used is designed to respect the symmetry condition expressed in the Nyquist Criterion '
|
||||
'avoiding inter-symbol interferences. Such a filter is called a Nyquist Filter. '
|
||||
'and the mimimum theoretical bandwidth (Roll-Off = zero) is called Nyquist Bandwidth.\n\n'
|
||||
'The Roll-Off or Excess of Bandwidth is usually expressed as a percentage of the Nyquist Bandwidth.',
|
||||
'-iCIR-':'Receiver\'s C/I [dB]\n\nEffect of signal impairment associated with terminal implementation, '
|
||||
'expressed as a signal to noise ratio to be combined with the intrinsic Signal to Noise Ratio affecting'
|
||||
' the link.\n\nImpairments are multiple : Phase Noise of the radio front end, Quantization Noise of the'
|
||||
' receiver\'s Analog to Digital Conversion, effect of imperfect synchronisation ...\n\n'
|
||||
'The tool supports comma separated lists of C/I or C/N values expressed in dB. In addition to the '
|
||||
'overall receiver\'s impairments, one can use this feature to simulate more details : downlink '
|
||||
'interferences, LNB\'s phase noise, impairment of signal distribution ...\n\nNote that signal '
|
||||
'impairments associated with the satellite and the receiver are combined together with the link '
|
||||
'noise to evaluate the practical bit rate.',
|
||||
'-iPenalty-':'Implementation Penalty vs theory [dB]\n\nTurbo and Turbo-like Codes are known for getting '
|
||||
'"almost Shannon" performances. There are however still some implementation taxes '
|
||||
': codes always have a residual bit error rate, making it very low requires some CNR margin.\n\n'
|
||||
'Other practical aspects also cost signal\'s energy like time and frequency synchronisation, '
|
||||
'physical layer framing...\n\nDVB-S2x, using LDPC codes and modern modulation related features '
|
||||
'is typically 1 dB away of the Shannon Limit in Quasi Error Free operation. Real systems also have'
|
||||
' to take margins, considering a reasonable value of 0.5 dB, a total penalty of 1.5 dB can be '
|
||||
'considered as typical.\n\n'
|
||||
'Original Turbo codes designed with higher residual bit error rates can get much closer '
|
||||
'to the Shannon Limit. ',
|
||||
'-iOH-':'Higher Layers Overhead [%]\n\nThe practical usage of information bits is based on a breakdown '
|
||||
'in multiple communications layers, all spending bits for the logistics of carrying user bits.'
|
||||
'For example, the process of encapsulation of IP datagrams on a DVB-S2x physical layer using'
|
||||
' the GSE standard costs a few percents of net bit rate, spent in framing structures, integrity '
|
||||
'control bits ...\n\n'
|
||||
'In a modern efficient satellite forward communication system the overhead to IP costs typically 5%',
|
||||
'-iNBW-':'Nyquist Bandwidth\n\nThe modulated carrier is passing bits in groups mapped on modulation symbols.'
|
||||
'Satellite modulation schemes typically map from 1 to 8 bits on each symbol passing though the channel.'
|
||||
'The Bit Rate is directly linked to the symbol rate, the number of symbols per second passing '
|
||||
'the channel ( BR = SR . Number of Bits per Symbol ).\n\n'
|
||||
'To pass a bandwidth limited channel, symbols have to be mapped on pulses "filtered" to limit the '
|
||||
'bandwidth. Theoretically, filtering can be "brickwall", one symbol per second passing in 1 Hertz.'
|
||||
'Practically, an excess of bandwidth is required, also called "Roll-Off of the filter. '
|
||||
'The filter used is also designed to respect the symmetry condition expressed in the Nyquist Criterion '
|
||||
'avoiding inter-symbol interferences. Such a filter is thus called Nyquist Filter '
|
||||
'and the minimum theoretical bandwidth (Roll-Off = zero) is called Nyquist Bandwidth.',
|
||||
'-iCNRbw-':'Signal to Noise Ratio in Available BW\n\n Ratio of the Signal Power and the Noise Power Captured '
|
||||
' in the available bandwidth.',
|
||||
'-iCNRnyq-':'Signal to Noise Ratio in Nyquist BW\n\nRatio of the Signal Power and the Noise Power Captured '
|
||||
' in the Nyquist Bandwidth = Available Bandwidth / ( 1 + Roll-Off).',
|
||||
'-iCNRrcv-':'Signal to Noise Ratio at Receiver Output\n\nRatio of the Signal Power and the total Noise Power'
|
||||
' captured along the complete communication chain (at receiver ouptut). This ratio is the relevant one'
|
||||
' for real-life performance evaluation. It is computed by combining the Signal to Noise in the Nyquist '
|
||||
'Bandwidth, the Receiver\'s C/I and the Satellite\'s C/I. Note that these 2 items are themselves '
|
||||
'resulting of many items which can be detailed as comma separated lists.',
|
||||
'-iBRbw-':'Theoretical Bit Rate in Available BW\n\nBit Rate theoretically achieved with zero Roll-Off in '
|
||||
'the available bandwidth. This bit rate is given by a direct application of the Shannon Limit. '
|
||||
'The normalized bit rate expressed as a percentage of the bit rate at infinite bandwidth is also given '
|
||||
'as well as the spectral efficiency of the available bandwidth.',
|
||||
'-iBRnyq-':'Theoretical Bit Rate in Nyquist BW\n\nBit Rate theoretically achieved in '
|
||||
'the Nyquist bandwidth (after having removed the Nyquist Roll-Off from the available Bandwidth).'
|
||||
'This bit rate is given by a direct application of the Shannon Limit.\n\nThe normalized bit rate '
|
||||
'expressed as a percentage of the bit rate at infinite bandwidth is also given as well as the spectral '
|
||||
'efficiency of the available bandwidth.\n\nThe efficiency in bit per symbol is also given and does '
|
||||
'correspond to the classical spectral efficiency in the Nyquist bandwidth.',
|
||||
'-iBRrcv-':'Practical Physcial Layer Bit Rate\n\n Practical Bit Rate achieved using real-world conditions. '
|
||||
'This bit rate is evaluated by using the "all degradations included" signal to noise ratio'
|
||||
'in the Shannon\'s formula.'
|
||||
'This bit rate does correspond to the user bits of the Physical Layer Frames.',
|
||||
'-iBRhigh-':'Practical Higher Layers Bit Rate\n\nPractical Bit Rate achieved using real-world modulation '
|
||||
'and coding and modern encapsulation methods of higher layers strcutures.\n\nThis Bit Rate does '
|
||||
'typically correspond to the user bits of the IP datagrams',
|
||||
'-Satellite-':'The evaluation is decomposed in 3 sections:\n\n'
|
||||
'1. The satellite link : satellite transmitter and path to the receiver\'s location with '
|
||||
'associated key characteristics \n\n'
|
||||
'2. The radio front end : antenna and amplification unit capturing as much signal as possible '
|
||||
'and as little noise as possible\n\n'
|
||||
'3. The base-band processing unit : unit extracting from a modulated carrier the useful '
|
||||
'information bits.'
|
||||
' All key functions are usually performed via digital signal processing : Nyquist filtering, '
|
||||
'synchronisation, demodulation, error correction, higher layer "decapsulation"...\n\n'
|
||||
'All fields are initially filled with meaningful values, you should start the exploration by '
|
||||
'changing the straightforward parameters and keep the intimidating figures unchanged. '
|
||||
'All parameters are "clickable" for getting associated background information.',
|
||||
'-CRC1-': 'Cyclic Redundancy Check of the input parameters, for changes identification purposes.',
|
||||
'-CRC2-': 'Cyclic Redundancy Check of the input parameters, for changes identification purposes.',
|
||||
'-CRC3-': 'Cyclic Redundancy Check of the input parameters, for changes identification purposes.',
|
||||
'Advanced': 'The Shannon Limit is a very powerful tool to analyse communication systems\' design, trade offs.'
|
||||
'All capacity evaluations in this tool are based on direct application of this formula taking '
|
||||
'into account real world impairments via signal to noise combinations. With this approach, '
|
||||
'using the overall C/N evaluated for a practical communication link gives a good estimate of the '
|
||||
'capacity achievable.\n\nApplying in addition the known average penalty of real modulation and '
|
||||
'coding schemes makes it accurate enough for initial systems evaluations.\n\nThe analytic formulas '
|
||||
'derived from the Shannon Limit for given spectral efficiencies are also of great help to drive '
|
||||
'the thinking in practical trade-offs.\n\n'
|
||||
'Additional useful links for people interested in a theoretical immersion :'
|
||||
'https://en.wikipedia.org/wiki/Nyquist_ISI_criterion\n'
|
||||
'https://en.wikipedia.org/wiki/Error_correction_code#Forward_error_correction\n'
|
||||
'https://en.wikipedia.org/wiki/Viterbi_decoder\n'
|
||||
'https://en.wikipedia.org/wiki/Turbo_code\n'
|
||||
'https://en.wikipedia.org/wiki/DVB-S2\n'
|
||||
'https://en.wikipedia.org/wiki/OSI_model\n',
|
||||
'Help':'Recommendations for using the tool\n\nThe first purpose of the tool is educational, allowing people to '
|
||||
'better understand the physics of communications and the role of key parameters\n\n'
|
||||
'All labels can be \"clicked\" to get information about associated item. All values '
|
||||
'(including this text) can be copy/pasted for further usage.\n\n'
|
||||
'The user should try multiple values in the fields one per one (starting from the least intimidating), '
|
||||
'explore the graphs and try to understand the underlying physics.\n\n'
|
||||
'The units for the different figures are as explicit as possible to facilitate the exploration.\n\n'
|
||||
'Despite the simplicity of the approach, the tool can also be useful to do a quick analysis of a '
|
||||
'communication link with a first order approach, avoiding the trap of the illusion of precision.\n\n'
|
||||
|
||||
}
|
||||
|
||||
159
app.py
Normal file
159
app.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Shannon Equation for Dummies — Streamlit Application
|
||||
|
||||
Main entry point with sidebar navigation.
|
||||
Designed for containerized deployment (Docker), multi-user, 24/7 operation.
|
||||
|
||||
Run with:
|
||||
streamlit run app.py --server.port 8080 --server.address 0.0.0.0
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Page Configuration (must be first Streamlit call)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
st.set_page_config(
|
||||
page_title="Shannon's Equation for Dummies",
|
||||
page_icon="📡",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Custom CSS for a modern, clean look
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* Main container */
|
||||
.block-container {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Metric styling */
|
||||
[data-testid="stMetric"] {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
border: 1px solid #0f3460;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
[data-testid="stMetricLabel"] {
|
||||
font-size: 0.85rem !important;
|
||||
color: #a0aec0 !important;
|
||||
}
|
||||
|
||||
[data-testid="stMetricValue"] {
|
||||
font-size: 1.1rem !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
[data-testid="stSidebar"] {
|
||||
background: linear-gradient(180deg, #0d1b2a 0%, #1b2838 100%);
|
||||
}
|
||||
|
||||
/* Tab styling */
|
||||
.stTabs [data-baseweb="tab-list"] {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stTabs [data-baseweb="tab"] {
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
/* Dividers */
|
||||
hr {
|
||||
border-color: #1e3a5f !important;
|
||||
}
|
||||
|
||||
/* Expanders */
|
||||
.streamlit-expanderHeader {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: #4FC3F7 !important;
|
||||
}
|
||||
|
||||
/* Info boxes */
|
||||
.stAlert {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Hide Deploy button and hamburger menu */
|
||||
[data-testid="stMainMenu"],
|
||||
[data-testid="stToolbar"] {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Sidebar Navigation
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
with st.sidebar:
|
||||
st.markdown("## 📡 Shannon for Dummies")
|
||||
st.caption("Educational Application — AP Feb 2021")
|
||||
st.divider()
|
||||
|
||||
page = st.radio(
|
||||
"Navigation",
|
||||
options=["animation", "orbits", "sat_types", "theory", "real_world", "contributions"],
|
||||
format_func=lambda x: {
|
||||
"animation": "📡 Satellite Link Animation",
|
||||
"orbits": "🌍 GEO / MEO / LEO Orbits",
|
||||
"sat_types": "🛰️ Satellite Missions & Types",
|
||||
"theory": "🧮 Theoretical Exploration",
|
||||
"real_world": "🛰️ Real World Link Budget",
|
||||
"contributions": "💬 Community Contributions",
|
||||
}[x],
|
||||
label_visibility="collapsed",
|
||||
)
|
||||
|
||||
st.divider()
|
||||
|
||||
st.markdown(
|
||||
"**About**\n\n"
|
||||
"Exploration from Claude Shannon's initial theory "
|
||||
"to its practical application to satellite communications.\n\n"
|
||||
"Built with [Streamlit](https://streamlit.io) · "
|
||||
"[Plotly](https://plotly.com)"
|
||||
)
|
||||
|
||||
st.caption("© 2021-2026 · Shannon Equation for Dummies")
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Page Routing
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
if page == "animation":
|
||||
from views.satellite_animation import render
|
||||
render()
|
||||
elif page == "orbits":
|
||||
from views.orbits_animation import render
|
||||
render()
|
||||
elif page == "sat_types":
|
||||
from views.satellite_types import render
|
||||
render()
|
||||
elif page == "theory":
|
||||
from views.theory import render
|
||||
render()
|
||||
elif page == "real_world":
|
||||
from views.real_world import render
|
||||
render()
|
||||
elif page == "contributions":
|
||||
from views.contributions import render
|
||||
render()
|
||||
@@ -1,2 +0,0 @@
|
||||
docker build -t my-python-app .
|
||||
docker run -it --rm --name my-running-app --network=nginx-proxy --env VIRTUAL_HOST=shannon.antopoid.com --env LETSENCRYPT_HOST=shannon.antopoid.com --env LETSENCRYPT_EMAIL=poidevin.freeboxos.fr@gmail.com my-python-app
|
||||
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
BIN
core/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
core/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/calculations.cpython-313.pyc
Normal file
BIN
core/__pycache__/calculations.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/database.cpython-313.pyc
Normal file
BIN
core/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/help_texts.cpython-313.pyc
Normal file
BIN
core/__pycache__/help_texts.cpython-313.pyc
Normal file
Binary file not shown.
280
core/calculations.py
Normal file
280
core/calculations.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Shannon Equation - Core Calculations Module
|
||||
|
||||
All scientific computation functions extracted from the original Shannon.py.
|
||||
These are pure functions with no UI dependency.
|
||||
"""
|
||||
|
||||
from math import log, pi, sqrt, cos, acos, atan
|
||||
import numpy as np
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Fundamental Shannon Functions
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def combine_cnr(*cnr_values: float) -> float:
|
||||
"""Combine multiple Carrier-to-Noise Ratios (in dB) into one equivalent C/N.
|
||||
|
||||
Uses the summation of normalized noise variances:
|
||||
1/CNR_total = sum(1/CNR_i)
|
||||
"""
|
||||
ncr_linear = 0.0
|
||||
for cnr_db in cnr_values:
|
||||
ncr_linear += 10 ** (-cnr_db / 10)
|
||||
return -10 * log(ncr_linear, 10)
|
||||
|
||||
|
||||
def shannon_capacity(bw: float = 36.0, cnr: float = 10.0, penalty: float = 0.0) -> float:
|
||||
"""Shannon channel capacity (bit rate in Mbps).
|
||||
|
||||
Args:
|
||||
bw: Bandwidth in MHz.
|
||||
cnr: Carrier-to-Noise Ratio in dB.
|
||||
penalty: Implementation penalty in dB.
|
||||
"""
|
||||
cnr_linear = 10 ** ((cnr - penalty) / 10)
|
||||
return bw * log(1 + cnr_linear, 2)
|
||||
|
||||
|
||||
def br_multiplier(bw_mul: float = 1.0, p_mul: float = 2.0, cnr: float = 10.0) -> float:
|
||||
"""Bit Rate multiplying factor when BW and Power are scaled."""
|
||||
cnr_linear = 10 ** (cnr / 10)
|
||||
return bw_mul * log(1 + cnr_linear * p_mul / bw_mul, 2) / log(1 + cnr_linear, 2)
|
||||
|
||||
|
||||
def shannon_points(bw: float = 36.0, cnr: float = 10.0):
|
||||
"""Compute key Shannon operating points.
|
||||
|
||||
Returns:
|
||||
(cnr_linear, br_infinity, c_n0, br_constrained)
|
||||
"""
|
||||
cnr_linear = 10 ** (cnr / 10)
|
||||
c_n0 = cnr_linear * bw
|
||||
br_infinity = c_n0 / log(2)
|
||||
br_constrained = shannon_capacity(bw, cnr)
|
||||
return cnr_linear, br_infinity, c_n0, br_constrained
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Satellite Link Budget Calculations
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def compute_satellite_link(
|
||||
freq_ghz: float,
|
||||
hpa_power_w: float,
|
||||
sat_loss_db: float,
|
||||
sat_cir_list: list[float],
|
||||
sat_beam_deg: float,
|
||||
gain_offset_db: float,
|
||||
sat_alt_km: float,
|
||||
sat_lat: float,
|
||||
sat_lon: float,
|
||||
gs_lat: float,
|
||||
gs_lon: float,
|
||||
availability_pct: float,
|
||||
) -> dict:
|
||||
"""Compute all satellite link parameters.
|
||||
|
||||
Returns a dict with all computed values.
|
||||
"""
|
||||
import itur
|
||||
|
||||
R_EARTH = 6378 # km
|
||||
SAT_ANT_EFF = 0.65
|
||||
|
||||
# Signal power after losses
|
||||
sig_power = hpa_power_w * 10 ** (-sat_loss_db / 10)
|
||||
|
||||
# Satellite antenna
|
||||
wavelength = 300e6 / freq_ghz / 1e9 # meters
|
||||
sat_gain_linear = SAT_ANT_EFF * (pi * 70 / sat_beam_deg) ** 2
|
||||
sat_gain_linear *= 10 ** (-gain_offset_db / 10)
|
||||
|
||||
# EIRP
|
||||
eirp_linear = sig_power * sat_gain_linear
|
||||
|
||||
# Satellite C/I
|
||||
sat_cir = combine_cnr(*sat_cir_list)
|
||||
|
||||
# Path geometry
|
||||
path_length = sqrt(
|
||||
sat_alt_km ** 2
|
||||
+ 2 * R_EARTH * (R_EARTH + sat_alt_km)
|
||||
* (1 - cos(np.radians(sat_lat - gs_lat)) * cos(np.radians(sat_lon - gs_lon)))
|
||||
)
|
||||
|
||||
phi = acos(cos(np.radians(sat_lat - gs_lat)) * cos(np.radians(sat_lon - gs_lon)))
|
||||
if phi > 0:
|
||||
elevation = float(np.degrees(
|
||||
atan((cos(phi) - R_EARTH / (R_EARTH + sat_alt_km)) / sqrt(1 - cos(phi) ** 2))
|
||||
))
|
||||
else:
|
||||
elevation = 90.0
|
||||
|
||||
# Atmospheric attenuation
|
||||
if elevation <= 0:
|
||||
atm_loss_db = 999.0
|
||||
else:
|
||||
atm_loss_db = float(
|
||||
itur.atmospheric_attenuation_slant_path(
|
||||
gs_lat, gs_lon, freq_ghz, elevation, 100 - availability_pct, 1
|
||||
).value
|
||||
)
|
||||
|
||||
# Path dispersion
|
||||
path_loss_linear = 4 * pi * (path_length * 1000) ** 2
|
||||
free_space_loss_linear = (4 * pi * path_length * 1000 / wavelength) ** 2
|
||||
|
||||
# PFD
|
||||
pfd_linear = eirp_linear / path_loss_linear * 10 ** (-atm_loss_db / 10)
|
||||
|
||||
return {
|
||||
"sig_power": sig_power,
|
||||
"wavelength": wavelength,
|
||||
"sat_gain_linear": sat_gain_linear,
|
||||
"eirp_linear": eirp_linear,
|
||||
"sat_cir": sat_cir,
|
||||
"path_length": path_length,
|
||||
"elevation": elevation,
|
||||
"atm_loss_db": atm_loss_db,
|
||||
"path_loss_linear": path_loss_linear,
|
||||
"free_space_loss_linear": free_space_loss_linear,
|
||||
"pfd_linear": pfd_linear,
|
||||
}
|
||||
|
||||
|
||||
def compute_receiver(
|
||||
pfd_linear: float,
|
||||
atm_loss_db: float,
|
||||
wavelength: float,
|
||||
cpe_ant_d: float,
|
||||
cpe_t_clear: float,
|
||||
) -> dict:
|
||||
"""Compute receiver-side parameters."""
|
||||
CPE_ANT_EFF = 0.6
|
||||
K_BOLTZ = 1.38e-23 # J/K
|
||||
|
||||
cpe_t_att = (cpe_t_clear - 40) + 40 * 10 ** (-atm_loss_db / 10) + 290 * (1 - 10 ** (-atm_loss_db / 10))
|
||||
|
||||
cpe_ae = pi * cpe_ant_d ** 2 / 4 * CPE_ANT_EFF
|
||||
cpe_gain_linear = (pi * cpe_ant_d / wavelength) ** 2 * CPE_ANT_EFF
|
||||
cpe_g_t = 10 * log(cpe_gain_linear / cpe_t_att, 10)
|
||||
|
||||
rx_power = pfd_linear * cpe_ae
|
||||
n0 = K_BOLTZ * cpe_t_att
|
||||
c_n0_hz = rx_power / n0
|
||||
c_n0_mhz = c_n0_hz / 1e6
|
||||
|
||||
br_infinity = c_n0_mhz / log(2)
|
||||
|
||||
# Spectral efficiency points
|
||||
bw_spe_1 = c_n0_mhz
|
||||
bw_spe_double = c_n0_mhz / (2 ** 2 - 1)
|
||||
|
||||
br_spe_1 = bw_spe_1
|
||||
br_spe_double = bw_spe_double * 2
|
||||
|
||||
return {
|
||||
"cpe_ae": cpe_ae,
|
||||
"cpe_gain_linear": cpe_gain_linear,
|
||||
"cpe_g_t": cpe_g_t,
|
||||
"cpe_t_att": cpe_t_att,
|
||||
"rx_power": rx_power,
|
||||
"n0": n0,
|
||||
"c_n0_hz": c_n0_hz,
|
||||
"c_n0_mhz": c_n0_mhz,
|
||||
"br_infinity": br_infinity,
|
||||
"bw_spe_1": bw_spe_1,
|
||||
"br_spe_1": br_spe_1,
|
||||
"br_spe_double": br_spe_double,
|
||||
}
|
||||
|
||||
|
||||
def compute_baseband(
|
||||
c_n0_mhz: float,
|
||||
br_infinity: float,
|
||||
bw_spe_1: float,
|
||||
sat_cir: float,
|
||||
bandwidth: float,
|
||||
rolloff: float,
|
||||
overheads: float,
|
||||
cnr_imp_list: list[float],
|
||||
penalties: float,
|
||||
) -> dict:
|
||||
"""Compute baseband processing results."""
|
||||
cnr_imp = combine_cnr(*cnr_imp_list)
|
||||
|
||||
cnr_spe_1 = 0.0 # dB
|
||||
cnr_bw = cnr_spe_1 + 10 * log(bw_spe_1 / bandwidth, 10)
|
||||
bw_nyq = bandwidth / (1 + rolloff / 100)
|
||||
cnr_nyq = cnr_spe_1 + 10 * log(bw_spe_1 / bw_nyq, 10)
|
||||
cnr_rcv = combine_cnr(cnr_nyq, cnr_imp, sat_cir)
|
||||
|
||||
br_nyq = shannon_capacity(bw_nyq, cnr_nyq)
|
||||
br_rcv = shannon_capacity(bw_nyq, cnr_rcv, penalties)
|
||||
br_rcv_higher = br_rcv / (1 + overheads / 100)
|
||||
|
||||
spe_nyq = br_nyq / bandwidth
|
||||
bits_per_symbol = br_nyq / bw_nyq
|
||||
spe_rcv = br_rcv / bandwidth
|
||||
spe_higher = br_rcv_higher / bandwidth
|
||||
|
||||
return {
|
||||
"cnr_bw": cnr_bw,
|
||||
"cnr_nyq": cnr_nyq,
|
||||
"cnr_rcv": cnr_rcv,
|
||||
"cnr_imp": cnr_imp,
|
||||
"bw_nyq": bw_nyq,
|
||||
"br_nyq": br_nyq,
|
||||
"br_rcv": br_rcv,
|
||||
"br_rcv_higher": br_rcv_higher,
|
||||
"br_nyq_norm": br_nyq / br_infinity,
|
||||
"br_rcv_norm": br_rcv / br_infinity,
|
||||
"br_rcv_h_norm": br_rcv_higher / br_infinity,
|
||||
"spe_nyq": spe_nyq,
|
||||
"bits_per_symbol": bits_per_symbol,
|
||||
"spe_rcv": spe_rcv,
|
||||
"spe_higher": spe_higher,
|
||||
"bandwidth": bandwidth,
|
||||
"rolloff": rolloff,
|
||||
"overheads": overheads,
|
||||
"penalties": penalties,
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Formatting Helpers
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def fmt_br(br: float) -> str:
|
||||
return f"{br:.1f} Mbps"
|
||||
|
||||
|
||||
def fmt_power(p: float) -> str:
|
||||
p_db = 10 * log(p, 10)
|
||||
if 1 < p < 1e4:
|
||||
return f"{p:.1f} W · {p_db:.1f} dBW"
|
||||
elif 1e-3 < p <= 1:
|
||||
return f"{p:.4f} W · {p_db:.1f} dBW"
|
||||
else:
|
||||
return f"{p:.1e} W · {p_db:.1f} dBW"
|
||||
|
||||
|
||||
def fmt_pfd(p: float) -> str:
|
||||
p_db = 10 * log(p, 10)
|
||||
return f"{p:.1e} W/m² · {p_db:.1f} dBW/m²"
|
||||
|
||||
|
||||
def fmt_psd(p: float) -> str:
|
||||
p_db = 10 * log(p, 10)
|
||||
return f"{p:.1e} W/MHz · {p_db:.1f} dBW/MHz"
|
||||
|
||||
|
||||
def fmt_gain(g: float) -> str:
|
||||
g_db = 10 * log(g, 10)
|
||||
return f"{g:.1f} · {g_db:.1f} dBi"
|
||||
|
||||
|
||||
def fmt_ploss(loss: float) -> str:
|
||||
loss_db = 10 * log(loss, 10)
|
||||
return f"{loss:.2e} m² · {loss_db:.1f} dBm²"
|
||||
107
core/database.py
Normal file
107
core/database.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Database module for managing user contributions.
|
||||
|
||||
Uses parameterized queries (no SQL injection) and context managers.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
DB_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
|
||||
|
||||
|
||||
def _get_db_path(db_name: str) -> str:
|
||||
os.makedirs(DB_DIR, exist_ok=True)
|
||||
return os.path.join(DB_DIR, db_name)
|
||||
|
||||
|
||||
def _init_db(db_path: str):
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
conn.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS contributions (
|
||||
num INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
keywords TEXT,
|
||||
text TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
password TEXT DEFAULT ''
|
||||
)"""
|
||||
)
|
||||
|
||||
|
||||
def write_contribution(
|
||||
db_name: str, name: str, title: str, keywords: str, text: str, password: str = ""
|
||||
) -> int:
|
||||
"""Write a new contribution. Returns the new contribution ID."""
|
||||
db_path = _get_db_path(db_name)
|
||||
_init_db(db_path)
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COALESCE(MAX(num), 0) FROM contributions")
|
||||
next_id = cursor.fetchone()[0] + 1
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO contributions (num, name, title, keywords, text, date, password) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(next_id, name, title, keywords, text, datetime.now().strftime("%Y-%m-%d"), password),
|
||||
)
|
||||
return next_id
|
||||
|
||||
|
||||
def search_contributions(
|
||||
db_name: str,
|
||||
name_filter: str = "",
|
||||
title_filter: str = "",
|
||||
keywords_filter: str = "",
|
||||
content_filter: str = "",
|
||||
limit: int = 50,
|
||||
) -> list[dict]:
|
||||
"""Search contributions with optional filters. Returns list of dicts."""
|
||||
db_path = _get_db_path(db_name)
|
||||
if not os.path.isfile(db_path):
|
||||
return []
|
||||
|
||||
_init_db(db_path)
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""SELECT num, name, title, keywords, text, date, password
|
||||
FROM contributions
|
||||
WHERE name LIKE ? AND title LIKE ? AND keywords LIKE ? AND text LIKE ?
|
||||
ORDER BY num DESC
|
||||
LIMIT ?""",
|
||||
(
|
||||
f"%{name_filter}%",
|
||||
f"%{title_filter}%",
|
||||
f"%{keywords_filter}%",
|
||||
f"%{content_filter}%",
|
||||
limit,
|
||||
),
|
||||
)
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
|
||||
def delete_contribution(db_name: str, num: int, password: str) -> bool:
|
||||
"""Delete a contribution if the password matches. Returns True on success."""
|
||||
db_path = _get_db_path(db_name)
|
||||
if not os.path.isfile(db_path):
|
||||
return False
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT password FROM contributions WHERE num = ?", (num,)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if row is None:
|
||||
return False
|
||||
if row[0] != password:
|
||||
return False
|
||||
cursor.execute("DELETE FROM contributions WHERE num = ?", (num,))
|
||||
return True
|
||||
297
core/help_texts.py
Normal file
297
core/help_texts.py
Normal file
@@ -0,0 +1,297 @@
|
||||
"""
|
||||
Help texts for Shannon application.
|
||||
|
||||
Cleaned-up version of Shannon_Dict.py, organized by section.
|
||||
Unicode subscripts/superscripts replaced by readable equivalents for web display.
|
||||
"""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Panel 1: Theoretical Exploration
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
THEORY_HELP = {
|
||||
"cnr": (
|
||||
"**Reference C/N [dB]**\n\n"
|
||||
"Reference Carrier to Noise Ratio in decibels: 10·log(C/N), where C is the "
|
||||
"Carrier's power and N is the Noise's Power, both measured in the reference "
|
||||
"Channel Bandwidth.\n\n"
|
||||
"The Carrier's power is often named Signal's Power and the Carrier to Noise Ratio "
|
||||
"is often named Signal to Noise Ratio."
|
||||
),
|
||||
"bw": (
|
||||
"**Reference BW [MHz]**\n\n"
|
||||
"Reference Channel Bandwidth — a key parameter of the communication channel.\n\n"
|
||||
"This bandwidth is usually a degree of freedom of the system design, eventually "
|
||||
"constrained by technological constraints and various kinds of frequency usage regulations."
|
||||
),
|
||||
"c_n0": (
|
||||
"**Carrier Power to Noise Power Density Ratio: C/N₀**\n\n"
|
||||
"Carrier's power (in Watts) divided by the Noise Spectral Power Density "
|
||||
"(in Watts per MHz). The result's units are MHz."
|
||||
),
|
||||
"br_inf": (
|
||||
"**Theoretical BR at infinite BW: 1.44·C/N₀**\n\n"
|
||||
"Bit Rate theoretically achievable when the signal occupies an infinite Bandwidth. "
|
||||
"This value is a useful asymptotic limit."
|
||||
),
|
||||
"br_unit": (
|
||||
"**Theoretical BR at Spectral Efficiency = 1: C/N₀**\n\n"
|
||||
"Bit Rate theoretically achievable at a Spectral Efficiency = 1: "
|
||||
"Bit Rate in Mbps = Bandwidth in MHz.\n\n"
|
||||
"The corresponding value, deduced from Shannon's formula, is given by C/N₀."
|
||||
),
|
||||
"br_bw": (
|
||||
"**Theoretical BR at Reference (BW, C/N)**\n\n"
|
||||
"Bit Rate theoretically achievable when the Bandwidth is constrained "
|
||||
"to the given reference value."
|
||||
),
|
||||
"cnr_lin": (
|
||||
"**C/N = C / (N₀·B)**\n\n"
|
||||
"Reference Carrier to Noise Ratio (or Signal to Noise Ratio). "
|
||||
"The C/N Ratio is usually given in dB: 10·log(C/N).\n\n"
|
||||
"Although the logarithm is convenient for many evaluations (multiplications become additions), "
|
||||
"it's also good to consider the ratio itself (Linear Format) to get some physical sense "
|
||||
"of the power ratio.\n\n"
|
||||
"The Carrier to Noise Ratio in linear format is the value used in Shannon's formula."
|
||||
),
|
||||
"br_mul": (
|
||||
"**Bit Rate Increase Factor**\n\n"
|
||||
"Bit Rate multiplying factor achieved when the Bandwidth and the Power "
|
||||
"are multiplied by a given set of values."
|
||||
),
|
||||
"bw_mul": (
|
||||
"**BW Increase Factor**\n\n"
|
||||
"Arbitrary multiplying factor applied to the Carrier's Bandwidth, for sensitivity analysis."
|
||||
),
|
||||
"p_mul": (
|
||||
"**Power Increase Factor**\n\n"
|
||||
"Arbitrary multiplying factor applied to the Carrier's Power, for sensitivity analysis."
|
||||
),
|
||||
"shannon": (
|
||||
"**The Shannon Limit** allows to evaluate the theoretical capacity achievable over "
|
||||
"a communication channel.\n\n"
|
||||
"As a true genius, Claude Shannon founded communication theory, information theory "
|
||||
"and more (click the Wikipedia button for more info).\n\n"
|
||||
"This equation is fundamental for the evaluation of communication systems. "
|
||||
"It is an apparently simple but extremely powerful tool to guide communication systems' designs.\n\n"
|
||||
"This equation tells us what is achievable, not how to achieve it. It took almost 50 years "
|
||||
"to approach this limit with the invention of Turbo codes.\n\n"
|
||||
"In the satellite domain, DVB-S2x, using LDPC codes iteratively decoded (Turbo-Like), "
|
||||
"is only 1 dB away from this limit."
|
||||
),
|
||||
"advanced": (
|
||||
"**AWGN Channel Model**\n\n"
|
||||
"The model assumes that the communication channel is **AWGN** (Additive White Gaussian Noise). "
|
||||
"This noise is supposed to be random and white, meaning noise at a given time is independent "
|
||||
"of noise at any other time — implying a flat and infinite spectrum. "
|
||||
"This noise is also supposed to be Gaussian.\n\n"
|
||||
"Although these assumptions seem very strong, they accurately match the cases of interest. "
|
||||
"Many impairments are actually non-linear and/or non-additive, but combining equivalent C/N "
|
||||
"of all impairments as if they were fully AWGN is in most cases very accurate. "
|
||||
"The reason is that the sum of random variables of unknown laws always tends to Gaussian, "
|
||||
"and thermal noise is dominating, actually white and gaussian, and whitening the rest.\n\n"
|
||||
"The tool accepts lists of comma-separated CNRs which will be combined in that way.\n\n"
|
||||
"Overall, the Shannon Limit is a pretty convenient tool to predict the real performances "
|
||||
"of communication systems."
|
||||
),
|
||||
"help": (
|
||||
"**Recommendations for using the tool**\n\n"
|
||||
"The first purpose of the tool is educational, allowing people to better understand "
|
||||
"the physics of communications and the role of key parameters.\n\n"
|
||||
"- Try multiple values in all the fields one by one\n"
|
||||
"- Explore the graphs and try to understand the underlying physics\n"
|
||||
"- The units are as explicit as possible to facilitate the exploration\n"
|
||||
"- Click on the ℹ️ icons to get information about each parameter"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Panel 2: Real World (Satellite Link Budget)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
REAL_WORLD_HELP = {
|
||||
"freq": (
|
||||
"**Frequency [GHz]**\n\n"
|
||||
"Frequency of the electromagnetic wave supporting the communication.\n\n"
|
||||
"For satellite downlink, typical bands: L: 1.5 GHz, S: 2.2 GHz, C: 4 GHz, "
|
||||
"Ku: 12 GHz, Ka: 19 GHz, Q: 40 GHz"
|
||||
),
|
||||
"sat_alt": (
|
||||
"**Satellite Altitude [km]**\n\n"
|
||||
"The position of the satellite is expressed in latitude, longitude, altitude. "
|
||||
"A GEO satellite has a latitude of 0° and an altitude of 35786 km. "
|
||||
"LEO satellites have altitudes lower than 2000 km. "
|
||||
"MEO altitudes are between LEO and GEO (O3B constellation: 8063 km)."
|
||||
),
|
||||
"sat_latlon": (
|
||||
"**Satellite Latitude and Longitude [°]**\n\n"
|
||||
"The program doesn't simulate the orbit — any satellite coordinates can be used. "
|
||||
"A GEO satellite has a latitude of 0°."
|
||||
),
|
||||
"gs_latlon": (
|
||||
"**Ground Station Lat, Lon [°]**\n\n"
|
||||
"The position of the ground station affects link availability due to weather statistics "
|
||||
"(tropical regions have very heavy rains attenuating signals at high frequencies). "
|
||||
"It also impacts the elevation angle and overall path length.\n\n"
|
||||
"🔗 [Find coordinates](https://www.gps-coordinates.net)"
|
||||
),
|
||||
"availability": (
|
||||
"**Link Availability [%]**\n\n"
|
||||
"A high desired link availability corresponds to high signal attenuation: "
|
||||
"only rare and severe weather events exceeding this attenuation can interrupt the link.\n\n"
|
||||
"For example, at 99.9% availability, the attenuation considered is only exceeded 0.1% of the time."
|
||||
),
|
||||
"path_length": (
|
||||
"**Path Length [km] @ Elevation [°]**\n\n"
|
||||
"Distance from the satellite to the ground station and elevation angle. "
|
||||
"Minimum path length = satellite altitude (elevation = 90°). "
|
||||
"A negative elevation means the satellite is not visible."
|
||||
),
|
||||
"atm_loss": (
|
||||
"**Atmospheric Attenuation [dB]**\n\n"
|
||||
"The atmosphere affects radio wave propagation with attenuation from rain, clouds, "
|
||||
"scintillation, multi-path, sand/dust storms, and atmospheric gases.\n\n"
|
||||
"Typical attenuation exceeded 0.1% of the time in Europe from GEO:\n"
|
||||
"- Ku: 2.5 dB\n- Ka: 6.9 dB\n- Q: 22 dB\n\n"
|
||||
"Uses [ITU-Rpy](https://itu-rpy.readthedocs.io/en/latest/index.html)."
|
||||
),
|
||||
"hpa": (
|
||||
"**HPA Output Power [W]**\n\n"
|
||||
"Power of the High Power Amplifier at the satellite's last amplification stage. "
|
||||
"Some satellites operate at saturation (DTH), others in multicarrier mode with "
|
||||
"reduced power (3 dB Output Back Off is typical for VSAT)."
|
||||
),
|
||||
"sat_beam": (
|
||||
"**Satellite Beam Diameter [°]**\n\n"
|
||||
"Half-power beam width. Typical values: 0.4–1.4° for GEO HTS, 3–6° for GEO DTH."
|
||||
),
|
||||
"gain_offset": (
|
||||
"**Gain Offset from Peak [dB]**\n\n"
|
||||
"Simulates terminals not at beam peak. 3 dB = worst case in 3 dB beam (DTH). "
|
||||
"1 dB = typical median performance for single feed per beam HTS."
|
||||
),
|
||||
"losses": (
|
||||
"**Output Section Losses [dB]**\n\n"
|
||||
"Signal loss between HPA and antenna (filters, waveguides, switches). "
|
||||
"Typical: 2.5 dB for classical satellites, 1 dB for active antennas."
|
||||
),
|
||||
"sat_cir": (
|
||||
"**Satellite C/I [dB]**\n\n"
|
||||
"Signal impairments from satellite implementation: intermodulation, filtering, phase noise. "
|
||||
"Supports comma-separated lists to include uplink C/N, interferences, etc."
|
||||
),
|
||||
"output_power": "**Output Power [W]** — Signal power at antenna output carrying user information.",
|
||||
"sat_gain": (
|
||||
"**Satellite Antenna Gain**\n\n"
|
||||
"Ratio between signal radiated on-axis vs. isotropic antenna. "
|
||||
"Expressed in dBi (dB relative to isotropic antenna)."
|
||||
),
|
||||
"eirp": (
|
||||
"**Equivalent Isotropic Radiated Power (EIRP)**\n\n"
|
||||
"Product Power × Gain in Watts. Represents the power required for an isotropic antenna "
|
||||
"to match the directive antenna's radiation in that direction."
|
||||
),
|
||||
"path_loss": (
|
||||
"**Path Dispersion Loss**\n\n"
|
||||
"Free-space propagation loss — simply the surface of a sphere with radius = path length. "
|
||||
"Not actual absorption, just geometric spreading."
|
||||
),
|
||||
"pfd": (
|
||||
"**Power Flux Density**\n\n"
|
||||
"Signal power per square meter at the terminal side. "
|
||||
"Actual captured power = PFD × antenna effective area."
|
||||
),
|
||||
"cpe_ant": (
|
||||
"**Customer Antenna Size [m]**\n\n"
|
||||
"Parabolic antenna with state-of-the-art efficiency (~60%)."
|
||||
),
|
||||
"cpe_temp": (
|
||||
"**Noise Temperature [K]**\n\n"
|
||||
"Total receiver's clear-sky noise temperature. Includes all contributors: "
|
||||
"receiver, sky, ground seen by antenna. Default of 120 K is a reasonable typical value."
|
||||
),
|
||||
"cpe_gain": (
|
||||
"**Antenna Effective Area and G/T**\n\n"
|
||||
"G/T is the figure of merit of a receive antenna: Gain / Noise Temperature. "
|
||||
"In case of rain, the signal is punished twice: attenuation weakens it and "
|
||||
"the rain attenuator generates additional noise."
|
||||
),
|
||||
"rx_power": "**RX Power** — Extremely small power before amplification. This is 'C' in Shannon's equation.",
|
||||
"n0": "**Noise Power Density N₀** — Noise Spectral Power Density of the radio front end.",
|
||||
"br_inf": (
|
||||
"**Bit Rate at infinite BW** — Asymptotic limit: 1.443·C/N₀. Never achieved in practice."
|
||||
),
|
||||
"br_unit": "**Bit Rate at Spectral Efficiency=1** — Bandwidth = Bit Rate and C/N = 0 dB.",
|
||||
"br_double": (
|
||||
"**Bit Rate at Spectral Efficiency=2** — Bandwidth-efficient operating point "
|
||||
"(BW = 0.5 × BR), often considered typical."
|
||||
),
|
||||
"bandwidth": (
|
||||
"**Occupied Bandwidth [MHz]**\n\n"
|
||||
"Bandwidth occupied by the communication channel — a key degree of freedom in system design."
|
||||
),
|
||||
"rolloff": (
|
||||
"**Nyquist Filter Rolloff [%]**\n\n"
|
||||
"Excess bandwidth required beyond the theoretical Nyquist minimum. "
|
||||
"The Nyquist filter avoids inter-symbol interference."
|
||||
),
|
||||
"cir": (
|
||||
"**Receiver C/I [dB]**\n\n"
|
||||
"Signal impairments from terminal: phase noise, quantization, synchronization errors. "
|
||||
"Supports comma-separated lists."
|
||||
),
|
||||
"penalty": (
|
||||
"**Implementation Penalty [dB]**\n\n"
|
||||
"DVB-S2x with LDPC codes is typically 1 dB from Shannon Limit. "
|
||||
"With 0.5 dB margin, a total of 1.5 dB is typical."
|
||||
),
|
||||
"overhead": (
|
||||
"**Higher Layers Overhead [%]**\n\n"
|
||||
"Encapsulation cost (IP over DVB-S2x via GSE). Typically ~5% for modern systems."
|
||||
),
|
||||
"cnr_bw": "**SNR in Available BW** — Signal-to-Noise Ratio in the available bandwidth.",
|
||||
"cnr_nyq": "**SNR in Nyquist BW** — SNR in Nyquist BW = Available BW / (1 + Roll-Off).",
|
||||
"cnr_rcv": (
|
||||
"**SNR at Receiver Output** — Combining link noise, satellite C/I, and receiver C/I. "
|
||||
"This is the relevant ratio for real-life performance."
|
||||
),
|
||||
"br_nyq": (
|
||||
"**Theoretical Bit Rate** — Direct application of Shannon Limit in Nyquist BW. "
|
||||
"Efficiency in bits/symbol also shown."
|
||||
),
|
||||
"br_rcv": (
|
||||
"**Practical Physical Layer Bit Rate** — Using all-degradations-included SNR "
|
||||
"in Shannon's formula."
|
||||
),
|
||||
"br_high": (
|
||||
"**Practical Higher Layers Bit Rate** — Corresponds to user bits of IP datagrams."
|
||||
),
|
||||
"satellite": (
|
||||
"The evaluation is decomposed in 3 sections:\n\n"
|
||||
"1. **Satellite Link** — transmitter and path to receiver with key characteristics\n"
|
||||
"2. **Radio Front End** — antenna and amplification capturing signal with minimal noise\n"
|
||||
"3. **Baseband Unit** — digital signal processing: filtering, synchronization, "
|
||||
"demodulation, error correction, decapsulation\n\n"
|
||||
"All fields are initially filled with meaningful values. Start by changing the "
|
||||
"straightforward parameters. All parameters have help tooltips."
|
||||
),
|
||||
"advanced": (
|
||||
"**Advanced Analysis Notes**\n\n"
|
||||
"All capacity evaluations use direct application of the Shannon formula with real-world "
|
||||
"impairments via C/N combinations. Useful links:\n\n"
|
||||
"- [Nyquist ISI Criterion](https://en.wikipedia.org/wiki/Nyquist_ISI_criterion)\n"
|
||||
"- [Error Correction Codes](https://en.wikipedia.org/wiki/Error_correction_code)\n"
|
||||
"- [Viterbi Decoder](https://en.wikipedia.org/wiki/Viterbi_decoder)\n"
|
||||
"- [Turbo Codes](https://en.wikipedia.org/wiki/Turbo_code)\n"
|
||||
"- [DVB-S2](https://en.wikipedia.org/wiki/DVB-S2)\n"
|
||||
"- [OSI Model](https://en.wikipedia.org/wiki/OSI_model)"
|
||||
),
|
||||
"help": (
|
||||
"**Recommendations**\n\n"
|
||||
"- Try multiple values one by one, starting from the least intimidating\n"
|
||||
"- Explore the graphs to understand the physics\n"
|
||||
"- Units are as explicit as possible\n"
|
||||
"- The tool can also be used for a quick first-order link analysis"
|
||||
),
|
||||
}
|
||||
@@ -1,18 +1,36 @@
|
||||
version: '3.1'
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
python:
|
||||
image: python.slim:latest
|
||||
container_name: python
|
||||
shannon:
|
||||
deploy:
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.shannon.rule=Host(`shannon.antopoid.com`)
|
||||
- traefik.http.routers.shannon.entrypoints=websecure
|
||||
- traefik.http.routers.shannon.tls.certresolver=myhttpchallenge
|
||||
- traefik.http.services.shannon.loadbalancer.server.port=8080
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: shannon-streamlit
|
||||
restart: always
|
||||
environment:
|
||||
- VIRTUAL_HOST=shannon.antopoid.com
|
||||
- LETSENCRYPT_HOST=shannon.antopoid.com
|
||||
- LETSENCRYPT_EMAIL=poidevin.freeboxos.fr@gmail.com
|
||||
ports:
|
||||
- 8888:8080
|
||||
expose:
|
||||
- 8080
|
||||
volumes:
|
||||
# Persist SQLite databases across container restarts
|
||||
- shannon_data:/app/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/_stcore/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
shannon_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: nginx-proxy
|
||||
|
||||
|
||||
43
docker-stack.yml
Normal file
43
docker-stack.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
# Docker Stack pour déploiement en Swarm
|
||||
# Compatible avec CI/CD Gitea Actions
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
shannon:
|
||||
image: shannon/streamlit:${IMAGE_TAG:-latest}
|
||||
expose:
|
||||
- 8080
|
||||
environment:
|
||||
- STREAMLIT_SERVER_HEADLESS=true
|
||||
volumes:
|
||||
- shannon_data:/app/data
|
||||
networks:
|
||||
- traefik_traefikfront
|
||||
deploy:
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.shannon.rule=Host(`shannon.antopoid.com`)
|
||||
- traefik.http.routers.shannon.entrypoints=websecure
|
||||
- traefik.http.routers.shannon.tls.certresolver=myhttpchallenge
|
||||
- traefik.http.services.shannon.loadbalancer.server.port=8080
|
||||
- traefik.http.middlewares.shannon-proxy-headers.headers.customrequestheaders.X-Forwarded-For=
|
||||
- traefik.http.routers.shannon.middlewares=shannon-proxy-headers
|
||||
- traefik.http.services.shannon.loadbalancer.passhostheader=true
|
||||
placement:
|
||||
constraints:
|
||||
- "node.hostname==macmini"
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
order: start-first
|
||||
|
||||
volumes:
|
||||
shannon_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
traefik_traefikfront:
|
||||
external: true
|
||||
@@ -1,25 +1,19 @@
|
||||
HTMLParser
|
||||
tk-tools==0.16.0
|
||||
Flask==1.1.2
|
||||
Jinja2==3.0.3
|
||||
MarkupSafe==2.0.1
|
||||
astropy==5.3
|
||||
certifi==2021.10.8
|
||||
cycler==0.11.0
|
||||
fonttools==4.28.5
|
||||
itur==0.3.3
|
||||
kiwisolver==1.4.4
|
||||
matplotlib==3.7.2
|
||||
numpy==1.25
|
||||
packaging==21.3
|
||||
Pillow==10.0.0
|
||||
pyerfa==2.0.0.3
|
||||
pyparsing==3.0.6
|
||||
pyproj==3.6.0
|
||||
remi==2021.3.2
|
||||
PySimpleGUI==4.60.5
|
||||
python-dateutil==2.8.2
|
||||
PyYAML==6.0
|
||||
scipy==1.11.1
|
||||
six==1.16.0
|
||||
# Core scientific dependencies
|
||||
numpy>=1.25
|
||||
scipy>=1.11
|
||||
matplotlib>=3.7
|
||||
plotly>=5.18
|
||||
|
||||
# Streamlit
|
||||
streamlit>=1.30
|
||||
|
||||
# Satellite propagation model
|
||||
itur>=0.3.3
|
||||
|
||||
# Astronomy utilities
|
||||
astropy>=5.3
|
||||
|
||||
# Geospatial
|
||||
pyproj>=3.6
|
||||
|
||||
# Database (built-in sqlite3, no extra dep needed)
|
||||
|
||||
505
slim.report.json
505
slim.report.json
@@ -1,505 +0,0 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"engine": "linux|Transformer|1.40.4|d310b07567dc90763f5f27f94c618f057295b55d|2023-08-26_01:39:22AM",
|
||||
"containerized": false,
|
||||
"host_distro": {
|
||||
"name": "Ubuntu",
|
||||
"version": "22.04",
|
||||
"display_name": "Ubuntu 22.04.3 LTS"
|
||||
},
|
||||
"type": "build",
|
||||
"state": "done",
|
||||
"target_reference": "python:latest",
|
||||
"system": {
|
||||
"type": "",
|
||||
"release": "",
|
||||
"distro": {
|
||||
"name": "",
|
||||
"version": "",
|
||||
"display_name": ""
|
||||
}
|
||||
},
|
||||
"source_image": {
|
||||
"identity": {
|
||||
"id": "sha256:e13fe79153bbf089fb6bac4fc8710eae318c6aa124a1ba4aa609e1e136496543",
|
||||
"tags": [
|
||||
"latest"
|
||||
],
|
||||
"names": [
|
||||
"python:latest"
|
||||
]
|
||||
},
|
||||
"size": 1020536111,
|
||||
"size_human": "1.0 GB",
|
||||
"create_time": "2023-07-16T18:27:16Z",
|
||||
"docker_version": "20.10.21",
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"env_vars": [
|
||||
"PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"LANG=C.UTF-8",
|
||||
"GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D",
|
||||
"PYTHON_VERSION=3.11.4",
|
||||
"PYTHON_PIP_VERSION=23.1.2",
|
||||
"PYTHON_SETUPTOOLS_VERSION=65.5.1",
|
||||
"PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/0d8570dc44796f4369b652222cf176b3db6ac70e/public/get-pip.py",
|
||||
"PYTHON_GET_PIP_SHA256=96461deced5c2a487ddc65207ec5a9cffeca0d34e7af7ea1afc470ff0d746207"
|
||||
],
|
||||
"container_entry": {
|
||||
"exe_path": ""
|
||||
}
|
||||
},
|
||||
"minified_image_size": 267206999,
|
||||
"minified_image_size_human": "267 MB",
|
||||
"minified_image": "python.slim",
|
||||
"minified_image_has_data": true,
|
||||
"minified_by": 3.8192716314290855,
|
||||
"artifact_location": "/tmp/slim-state/.slim-state/images/e13fe79153bbf089fb6bac4fc8710eae318c6aa124a1ba4aa609e1e136496543/artifacts",
|
||||
"container_report_name": "creport.json",
|
||||
"seccomp_profile_name": "python-seccomp.json",
|
||||
"apparmor_profile_name": "python-apparmor-profile",
|
||||
"image_stack": [
|
||||
{
|
||||
"is_top_image": true,
|
||||
"id": "sha256:e13fe79153bbf089fb6bac4fc8710eae318c6aa124a1ba4aa609e1e136496543",
|
||||
"full_name": "python:latest",
|
||||
"repo_name": "python",
|
||||
"version_tag": "latest",
|
||||
"raw_tags": [
|
||||
"python:latest"
|
||||
],
|
||||
"create_time": "2023-07-16T18:27:16Z",
|
||||
"new_size": 1020536111,
|
||||
"new_size_human": "1.0 GB",
|
||||
"instructions": [
|
||||
{
|
||||
"type": "ADD",
|
||||
"time": "2023-07-04T01:19:58Z",
|
||||
"is_nop": true,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 74759018,
|
||||
"size_human": "75 MB",
|
||||
"params": "file:bd80a4461150784e5f2f5a1faa720cc347ad3e30ee0969adbfad574c316f5aef in /",
|
||||
"command_snippet": "ADD file:bd80a4461150784e5f2f5a1faa720cc347a...",
|
||||
"command_all": "ADD file:bd80a4461150784e5f2f5a1faa720cc347ad3e30ee0969adbfad574c316f5aef /",
|
||||
"target": "/",
|
||||
"source_type": "file",
|
||||
"inst_set_time_bucket": "2023-07-04T01:15:00Z",
|
||||
"inst_set_time_index": 1,
|
||||
"inst_set_time_reverse_index": 3
|
||||
},
|
||||
{
|
||||
"type": "CMD",
|
||||
"time": "2023-07-04T01:19:58Z",
|
||||
"is_nop": true,
|
||||
"is_exec_form": true,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "[\"bash\"]\n",
|
||||
"command_snippet": "CMD [\"bash\"]\n",
|
||||
"command_all": "CMD [\"bash\"]\n",
|
||||
"inst_set_time_bucket": "2023-07-04T01:15:00Z",
|
||||
"inst_set_time_index": 1,
|
||||
"inst_set_time_reverse_index": 3
|
||||
},
|
||||
{
|
||||
"type": "ENV",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"command_snippet": "ENV PATH=/usr/local/bin:/usr/local/sbin:/usr...",
|
||||
"command_all": "ENV PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "ENV",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "LANG=C.UTF-8",
|
||||
"command_snippet": "ENV LANG=C.UTF-8",
|
||||
"command_all": "ENV LANG=C.UTF-8",
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "RUN",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 9214160,
|
||||
"size_human": "9.2 MB",
|
||||
"command_snippet": "RUN set -eux; \tapt-get update; \tapt-get inst...",
|
||||
"command_all": "RUN set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tca-certificates \t\tnetbase \t\ttzdata \t; \trm -rf /var/lib/apt/lists/*",
|
||||
"system_commands": [
|
||||
"set -eux",
|
||||
"apt-get update",
|
||||
"apt-get install -y --no-install-recommends ca-certificates netbase tzdata",
|
||||
"rm -rf /var/lib/apt/lists/*"
|
||||
],
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "ENV",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D",
|
||||
"command_snippet": "ENV GPG_KEY=A035C8C19219BA821ECEA86B64E628F8...",
|
||||
"command_all": "ENV GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D",
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "ENV",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "PYTHON_VERSION=3.11.4",
|
||||
"command_snippet": "ENV PYTHON_VERSION=3.11.4",
|
||||
"command_all": "ENV PYTHON_VERSION=3.11.4",
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "RUN",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 53218187,
|
||||
"size_human": "53 MB",
|
||||
"command_snippet": "RUN set -eux; \t\tsavedAptMark=\"$(apt-mark sho...",
|
||||
"command_all": "RUN set -eux; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tdpkg-dev \t\tgcc \t\tgnupg \t\tlibbluetooth-dev \t\tlibbz2-dev \t\tlibc6-dev \t\tlibdb-dev \t\tlibexpat1-dev \t\tlibffi-dev \t\tlibgdbm-dev \t\tliblzma-dev \t\tlibncursesw5-dev \t\tlibreadline-dev \t\tlibsqlite3-dev \t\tlibssl-dev \t\tmake \t\ttk-dev \t\tuuid-dev \t\twget \t\txz-utils \t\tzlib1g-dev \t; \t\twget -O python.tar.xz \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz\"; \twget -O python.tar.xz.asc \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc\"; \tGNUPGHOME=\"$(mktemp -d)\"; export GNUPGHOME; \tgpg --batch --keyserver hkps://keys.openpgp.org --recv-keys \"$GPG_KEY\"; \tgpg --batch --verify python.tar.xz.asc python.tar.xz; \tgpgconf --kill all; \trm -rf \"$GNUPGHOME\" python.tar.xz.asc; \tmkdir -p /usr/src/python; \ttar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz; \trm python.tar.xz; \t\tcd /usr/src/python; \tgnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\"; \t./configure \t\t--build=\"$gnuArch\" \t\t--enable-loadable-sqlite-extensions \t\t--enable-optimizations \t\t--enable-option-checking=fatal \t\t--enable-shared \t\t--with-lto \t\t--with-system-expat \t\t--without-ensurepip \t; \tnproc=\"$(nproc)\"; \tEXTRA_CFLAGS=\"$(dpkg-buildflags --get CFLAGS)\"; \tLDFLAGS=\"$(dpkg-buildflags --get LDFLAGS)\"; \tLDFLAGS=\"${LDFLAGS:--Wl},--strip-all\"; \tmake -j \"$nproc\" \t\t\"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \t\t\"LDFLAGS=${LDFLAGS:-}\" \t\t\"PROFILE_TASK=${PROFILE_TASK:-}\" \t; \trm python; \tmake -j \"$nproc\" \t\t\"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \t\t\"LDFLAGS=${LDFLAGS:--Wl},-rpath='\\$\\$ORIGIN/../lib'\" \t\t\"PROFILE_TASK=${PROFILE_TASK:-}\" \t\tpython \t; \tmake install; \t\tcd /; \trm -rf /usr/src/python; \t\tfind /usr/local -depth \t\t\\( \t\t\t\\( -type d -a \\( -name test -o -name tests -o -name idle_test \\) \\) \t\t\t-o \\( -type f -a \\( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \\) \\) \t\t\\) -exec rm -rf '{}' + \t; \t\tldconfig; \t\tapt-mark auto '.*' > /dev/null; \tapt-mark manual $savedAptMark; \tfind /usr/local -type f -executable -not \\( -name '*tkinter*' \\) -exec ldd '{}' ';' \t\t| awk '/=>/ { so = $(NF-1); if (index(so, \"/usr/local/\") == 1) { next }; gsub(\"^/(usr/)?\", \"\", so); print so }' \t\t| sort -u \t\t| xargs -r dpkg-query --search \t\t| cut -d: -f1 \t\t| sort -u \t\t| xargs -r apt-mark manual \t; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \trm -rf /var/lib/apt/lists/*; \t\tpython3 --version",
|
||||
"system_commands": [
|
||||
"set -eux",
|
||||
"savedAptMark=\"$(apt-mark showmanual)\"",
|
||||
"apt-get update",
|
||||
"apt-get install -y --no-install-recommends dpkg-dev gcc gnupg libbluetooth-dev libbz2-dev libc6-dev libdb-dev libexpat1-dev libffi-dev libgdbm-dev liblzma-dev libncursesw5-dev libreadline-dev libsqlite3-dev libssl-dev make tk-dev uuid-dev wget xz-utils zlib1g-dev",
|
||||
"wget -O python.tar.xz \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz\"",
|
||||
"wget -O python.tar.xz.asc \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc\"",
|
||||
"GNUPGHOME=\"$(mktemp -d)\"",
|
||||
"export GNUPGHOME",
|
||||
"gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys \"$GPG_KEY\"",
|
||||
"gpg --batch --verify python.tar.xz.asc python.tar.xz",
|
||||
"gpgconf --kill all",
|
||||
"rm -rf \"$GNUPGHOME\" python.tar.xz.asc",
|
||||
"mkdir -p /usr/src/python",
|
||||
"tar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz",
|
||||
"rm python.tar.xz",
|
||||
"cd /usr/src/python",
|
||||
"gnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\"",
|
||||
"./configure --build=\"$gnuArch\" --enable-loadable-sqlite-extensions --enable-optimizations --enable-option-checking=fatal --enable-shared --with-lto --with-system-expat --without-ensurepip",
|
||||
"nproc=\"$(nproc)\"",
|
||||
"EXTRA_CFLAGS=\"$(dpkg-buildflags --get CFLAGS)\"",
|
||||
"LDFLAGS=\"$(dpkg-buildflags --get LDFLAGS)\"",
|
||||
"LDFLAGS=\"${LDFLAGS:--Wl},--strip-all\"",
|
||||
"make -j \"$nproc\" \"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \"LDFLAGS=${LDFLAGS:-}\" \"PROFILE_TASK=${PROFILE_TASK:-}\"",
|
||||
"rm python",
|
||||
"make -j \"$nproc\" \"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \"LDFLAGS=${LDFLAGS:--Wl},-rpath='$$ORIGIN/../lib'\" \"PROFILE_TASK=${PROFILE_TASK:-}\" python",
|
||||
"make install",
|
||||
"cd /",
|
||||
"rm -rf /usr/src/python",
|
||||
"find /usr/local -depth ( ( -type d -a ( -name test -o -name tests -o -name idle_test ) ) -o ( -type f -a ( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' ) ) ) -exec rm -rf '{}' +",
|
||||
"ldconfig",
|
||||
"apt-mark auto '.*' > /dev/null",
|
||||
"apt-mark manual $savedAptMark",
|
||||
"find /usr/local -type f -executable -not ( -name '*tkinter*' ) -exec ldd '{}' '",
|
||||
"' | awk '/=>/ { so = $(NF-1)",
|
||||
"if (index(so, \"/usr/local/\") == 1) { next }",
|
||||
"gsub(\"^/(usr/)?\", \"\", so)",
|
||||
"print so }' | sort -u | xargs -r dpkg-query --search | cut -d: -f1 | sort -u | xargs -r apt-mark manual",
|
||||
"apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false",
|
||||
"rm -rf /var/lib/apt/lists/*",
|
||||
"python3 --version"
|
||||
],
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "RUN",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 32,
|
||||
"size_human": "32 B",
|
||||
"command_snippet": "RUN set -eux; \tfor src in idle3 pydoc3 pytho...",
|
||||
"command_all": "RUN set -eux; \tfor src in idle3 pydoc3 python3 python3-config; do \t\tdst=\"$(echo \"$src\" | tr -d 3)\"; \t\t[ -s \"/usr/local/bin/$src\" ]; \t\t[ ! -e \"/usr/local/bin/$dst\" ]; \t\tln -svT \"$src\" \"/usr/local/bin/$dst\"; \tdone",
|
||||
"system_commands": [
|
||||
"set -eux",
|
||||
"for src in idle3 pydoc3 python3 python3-config",
|
||||
"do dst=\"$(echo \"$src\" | tr -d 3)\"",
|
||||
"[ -s \"/usr/local/bin/$src\" ]",
|
||||
"[ ! -e \"/usr/local/bin/$dst\" ]",
|
||||
"ln -svT \"$src\" \"/usr/local/bin/$dst\"",
|
||||
"done"
|
||||
],
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "ENV",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "PYTHON_PIP_VERSION=23.1.2",
|
||||
"command_snippet": "ENV PYTHON_PIP_VERSION=23.1.2",
|
||||
"command_all": "ENV PYTHON_PIP_VERSION=23.1.2",
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "ENV",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "PYTHON_SETUPTOOLS_VERSION=65.5.1",
|
||||
"command_snippet": "ENV PYTHON_SETUPTOOLS_VERSION=65.5.1",
|
||||
"command_all": "ENV PYTHON_SETUPTOOLS_VERSION=65.5.1",
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "ENV",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/0d8570dc44796f4369b652222cf176b3db6ac70e/public/get-pip.py",
|
||||
"command_snippet": "ENV PYTHON_GET_PIP_URL=https://github.com/py...",
|
||||
"command_all": "ENV PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/0d8570dc44796f4369b652222cf176b3db6ac70e/public/get-pip.py",
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "ENV",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "PYTHON_GET_PIP_SHA256=96461deced5c2a487ddc65207ec5a9cffeca0d34e7af7ea1afc470ff0d746207",
|
||||
"command_snippet": "ENV PYTHON_GET_PIP_SHA256=96461deced5c2a487d...",
|
||||
"command_all": "ENV PYTHON_GET_PIP_SHA256=96461deced5c2a487ddc65207ec5a9cffeca0d34e7af7ea1afc470ff0d746207",
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "RUN",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": false,
|
||||
"layer_index": 0,
|
||||
"size": 12240418,
|
||||
"size_human": "12 MB",
|
||||
"command_snippet": "RUN set -eux; \t\tsavedAptMark=\"$(apt-mark sho...",
|
||||
"command_all": "RUN set -eux; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends wget; \t\twget -O get-pip.py \"$PYTHON_GET_PIP_URL\"; \techo \"$PYTHON_GET_PIP_SHA256 *get-pip.py\" | sha256sum -c -; \t\tapt-mark auto '.*' > /dev/null; \t[ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark > /dev/null; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \trm -rf /var/lib/apt/lists/*; \t\texport PYTHONDONTWRITEBYTECODE=1; \t\tpython get-pip.py \t\t--disable-pip-version-check \t\t--no-cache-dir \t\t--no-compile \t\t\"pip==$PYTHON_PIP_VERSION\" \t\t\"setuptools==$PYTHON_SETUPTOOLS_VERSION\" \t; \trm -f get-pip.py; \t\tpip --version",
|
||||
"system_commands": [
|
||||
"set -eux",
|
||||
"savedAptMark=\"$(apt-mark showmanual)\"",
|
||||
"apt-get update",
|
||||
"apt-get install -y --no-install-recommends wget",
|
||||
"wget -O get-pip.py \"$PYTHON_GET_PIP_URL\"",
|
||||
"echo \"$PYTHON_GET_PIP_SHA256 *get-pip.py\" | sha256sum -c -",
|
||||
"apt-mark auto '.*' > /dev/null",
|
||||
"[ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark > /dev/null",
|
||||
"apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false",
|
||||
"rm -rf /var/lib/apt/lists/*",
|
||||
"export PYTHONDONTWRITEBYTECODE=1",
|
||||
"python get-pip.py --disable-pip-version-check --no-cache-dir --no-compile \"pip==$PYTHON_PIP_VERSION\" \"setuptools==$PYTHON_SETUPTOOLS_VERSION\"",
|
||||
"rm -f get-pip.py",
|
||||
"pip --version"
|
||||
],
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "CMD",
|
||||
"time": "2023-06-13T17:45:16Z",
|
||||
"is_nop": false,
|
||||
"is_exec_form": true,
|
||||
"local_image_exists": true,
|
||||
"intermediate_image_id": "sha256:be2470db10f711ec941d24bc9a489dd457b6000b624ee251e19a445ad9f38839",
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "[\"python3\"]\n",
|
||||
"command_snippet": "CMD [\"python3\"]\n",
|
||||
"command_all": "CMD [\"python3\"]\n",
|
||||
"comment": "buildkit.dockerfile.v0",
|
||||
"is_buildkit_instruction": true,
|
||||
"inst_set_time_bucket": "2023-06-13T17:45:00Z",
|
||||
"inst_set_time_index": 0,
|
||||
"inst_set_time_reverse_index": 4
|
||||
},
|
||||
{
|
||||
"type": "WORKDIR",
|
||||
"time": "2023-07-06T15:08:21Z",
|
||||
"is_nop": true,
|
||||
"local_image_exists": true,
|
||||
"intermediate_image_id": "sha256:ad187361307c8e838bb1f0a48b91e97d83d7aea811827b4d2bab393284f739e0",
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "/usr/src/app",
|
||||
"command_snippet": "WORKDIR /usr/src/app",
|
||||
"command_all": "WORKDIR /usr/src/app",
|
||||
"system_commands": [
|
||||
"mkdir -p /usr/src/app"
|
||||
],
|
||||
"inst_set_time_bucket": "2023-07-06T15:00:00Z",
|
||||
"inst_set_time_index": 2,
|
||||
"inst_set_time_reverse_index": 2
|
||||
},
|
||||
{
|
||||
"type": "COPY",
|
||||
"time": "2023-07-12T20:36:09Z",
|
||||
"is_nop": true,
|
||||
"local_image_exists": true,
|
||||
"intermediate_image_id": "sha256:a1b245cb980c1ac2c038a60bbe84fb8c65a1be6aedce32e15f29a53e4ea8e364",
|
||||
"layer_index": 0,
|
||||
"size": 398,
|
||||
"size_human": "398 B",
|
||||
"params": "file:1383fead6d13fe1a3d2822aaafeadc3c38b2cfeea627e71b14c63805820e09a2 in ./",
|
||||
"command_snippet": "COPY file:1383fead6d13fe1a3d2822aaafeadc3c38...",
|
||||
"command_all": "COPY file:1383fead6d13fe1a3d2822aaafeadc3c38b2cfeea627e71b14c63805820e09a2 ./",
|
||||
"target": "./",
|
||||
"source_type": "file",
|
||||
"inst_set_time_bucket": "2023-07-12T20:30:00Z",
|
||||
"inst_set_time_index": 3,
|
||||
"inst_set_time_reverse_index": 1
|
||||
},
|
||||
{
|
||||
"type": "RUN",
|
||||
"time": "2023-07-16T18:25:45Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": true,
|
||||
"intermediate_image_id": "sha256:0bffdc8daa291358931fff5ca204342be123ed040aa8352657c0a97dc0da7a1b",
|
||||
"layer_index": 0,
|
||||
"size": 36428885,
|
||||
"size_human": "36 MB",
|
||||
"command_snippet": "RUN apt-get update -y && \\\n\tapt-get install ...",
|
||||
"command_all": "RUN apt-get update -y && \\\n\tapt-get install -y tk tcl",
|
||||
"system_commands": [
|
||||
"apt-get update -y",
|
||||
"apt-get install -y tk tcl"
|
||||
],
|
||||
"inst_set_time_bucket": "2023-07-16T18:15:00Z",
|
||||
"inst_set_time_index": 4,
|
||||
"inst_set_time_reverse_index": 0
|
||||
},
|
||||
{
|
||||
"type": "RUN",
|
||||
"time": "2023-07-16T18:27:15Z",
|
||||
"is_nop": false,
|
||||
"local_image_exists": true,
|
||||
"intermediate_image_id": "sha256:1087081f44d63fc4d50ed96db8ee05d9ec956c1dbbcd8b125511ced95b4c1d7e",
|
||||
"layer_index": 0,
|
||||
"size": 833949399,
|
||||
"size_human": "834 MB",
|
||||
"command_snippet": "RUN pip install --force-reinstall -r require...",
|
||||
"command_all": "RUN pip install --force-reinstall -r requirements.txt",
|
||||
"system_commands": [
|
||||
"pip install --force-reinstall -r requirements.txt"
|
||||
],
|
||||
"inst_set_time_bucket": "2023-07-16T18:15:00Z",
|
||||
"inst_set_time_index": 4,
|
||||
"inst_set_time_reverse_index": 0
|
||||
},
|
||||
{
|
||||
"type": "COPY",
|
||||
"time": "2023-07-16T18:27:16Z",
|
||||
"is_nop": true,
|
||||
"local_image_exists": true,
|
||||
"intermediate_image_id": "sha256:7ae91476369b98ce3f0ec518e591385fdde2a0631944b93cb5672850c27086d5",
|
||||
"layer_index": 0,
|
||||
"size": 725614,
|
||||
"size_human": "726 kB",
|
||||
"params": "dir:c64af32925ea67cdf709617fb045107117c1bc58e9add8805e4e31a29cdfbc91 in .",
|
||||
"command_snippet": "COPY dir:c64af32925ea67cdf709617fb045107117c...",
|
||||
"command_all": "COPY dir:c64af32925ea67cdf709617fb045107117c1bc58e9add8805e4e31a29cdfbc91 .",
|
||||
"target": ".",
|
||||
"source_type": "dir",
|
||||
"inst_set_time_bucket": "2023-07-16T18:15:00Z",
|
||||
"inst_set_time_index": 4,
|
||||
"inst_set_time_reverse_index": 0
|
||||
},
|
||||
{
|
||||
"type": "CMD",
|
||||
"time": "2023-07-16T18:27:16Z",
|
||||
"is_last_instruction": true,
|
||||
"is_nop": true,
|
||||
"is_exec_form": true,
|
||||
"local_image_exists": true,
|
||||
"layer_index": 0,
|
||||
"size": 0,
|
||||
"params": "[\"python\",\"./Shannon.py\"]\n",
|
||||
"command_snippet": "CMD [\"python\",\"./Shannon.py\"]\n",
|
||||
"command_all": "CMD [\"python\",\"./Shannon.py\"]\n",
|
||||
"raw_tags": [
|
||||
"python:latest"
|
||||
],
|
||||
"inst_set_time_bucket": "2023-07-16T18:15:00Z",
|
||||
"inst_set_time_index": 4,
|
||||
"inst_set_time_reverse_index": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"image_created": true,
|
||||
"image_build_engine": "internal"
|
||||
}
|
||||
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