feat: add interactive exploration of Shannon's capacity formula with Plotly graphs
All checks were successful
Build & Deploy Shannon / 🏗️ Build & Deploy Shannon (push) Successful in 3m1s

- Implemented bandwidth sensitivity and power sensitivity plots.
- Created a contour map for bit rate multiplying factors.
- Added input parameters for C/N and bandwidth with validation.
- Displayed computed results and sensitivity analysis metrics.
- Integrated interactive graphs for user exploration.
- Included background information section for user guidance.
This commit is contained in:
Poidevin, Antoine (ITOP CM) - AF
2026-02-20 10:33:09 +01:00
parent beda405953
commit 6a4ccc3376
38 changed files with 4319 additions and 11161 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
__pycache__
*.pyc
*.pyo
.git
.gitignore
*.save
*.db
.venv
venv
README.md

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

View File

@@ -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"]

View File

@@ -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" ]

File diff suppressed because it is too large Load Diff

113
README.md
View File

@@ -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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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()

View File

@@ -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
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

280
core/calculations.py Normal file
View 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
View 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
View 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.41.4° for GEO HTS, 36° 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"
),
}

View File

@@ -1,18 +1,36 @@
version: '3.1'
version: '3.8'
services:
python:
image: python.slim:latest
container_name: python
restart: always
environment:
- VIRTUAL_HOST=shannon.antopoid.com
- LETSENCRYPT_HOST=shannon.antopoid.com
- LETSENCRYPT_EMAIL=poidevin.freeboxos.fr@gmail.com
ports:
- 8888:8080
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
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
default:
external:
name: nginx-proxy

43
docker-stack.yml Normal file
View 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

View File

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

View File

@@ -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
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

130
views/contributions.py Normal file
View File

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

585
views/orbits_animation.py Normal file
View File

@@ -0,0 +1,585 @@
"""
GEO / MEO / LEO Orbits — Interactive Animation
================================================
Animated comparison of satellite orbit types with key trade-offs.
"""
import streamlit as st
import streamlit.components.v1 as components
_ORBITS_HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: transparent; overflow: hidden; font-family: 'Segoe UI', system-ui, sans-serif; }
canvas { display: block; }
/* ── Info panel ── */
#info {
position: absolute; top: 14px; right: 18px;
background: rgba(13,27,42,0.92); border: 1px solid rgba(79,195,247,0.3);
border-radius: 14px; padding: 18px 20px; width: 280px;
backdrop-filter: blur(12px); box-shadow: 0 8px 32px rgba(0,0,0,0.45);
color: #e2e8f0; font-size: 12px; line-height: 1.7;
transition: all 0.3s;
}
#info h3 { color: #4FC3F7; font-size: 14px; margin-bottom: 10px; text-align: center; }
#info .orbit-name { font-weight: 700; font-size: 13px; }
#info .stat { display: flex; justify-content: space-between; padding: 3px 0; }
#info .stat .label { color: #94a3b8; }
#info .stat .value { color: #e2e8f0; font-weight: 600; }
#info hr { border: none; border-top: 1px solid rgba(79,195,247,0.15); margin: 8px 0; }
/* ── Speed slider ── */
#speedControl {
position: absolute; bottom: 14px; left: 18px;
background: rgba(13,27,42,0.88); border: 1px solid rgba(79,195,247,0.2);
border-radius: 12px; padding: 14px 18px; width: 220px;
backdrop-filter: blur(10px); color: #94a3b8; font-size: 12px;
}
#speedControl label { display: block; margin-bottom: 6px; color: #4FC3F7; font-weight: 600; }
#speedControl input[type=range] { width: 100%; accent-color: #4FC3F7; }
/* ── Legend ── */
#orbitLegend {
position: absolute; bottom: 14px; right: 18px;
background: rgba(13,27,42,0.88); border: 1px solid rgba(79,195,247,0.2);
border-radius: 12px; padding: 14px 18px; width: 280px;
backdrop-filter: blur(10px); color: #e2e8f0; font-size: 12px; line-height: 1.7;
}
#orbitLegend h4 { color: #4FC3F7; margin-bottom: 8px; font-size: 13px; }
.legend-item {
display: flex; align-items: center; gap: 10px; padding: 4px 0; cursor: pointer;
border-radius: 6px; padding: 4px 8px; transition: background 0.2s;
}
.legend-item:hover { background: rgba(79,195,247,0.08); }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.legend-label { flex: 1; }
.legend-detail { color: #64748b; font-size: 11px; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="info">
<h3>🛰️ Orbit Comparison</h3>
<div id="infoContent">
<p style="color:#94a3b8; text-align:center;">
Hover over a satellite to see its details.
</p>
</div>
</div>
<div id="speedControl">
<label>⏱️ Animation Speed</label>
<input type="range" id="speedSlider" min="0.1" max="5" step="0.1" value="1">
<div style="display:flex; justify-content:space-between; margin-top:4px;">
<span>Slow</span><span>Fast</span>
</div>
</div>
<div id="orbitLegend">
<h4>📐 Orbit Types</h4>
<div class="legend-item" data-orbit="leo">
<span class="legend-dot" style="background:#34d399"></span>
<span class="legend-label">LEO</span>
<span class="legend-detail">160 2 000 km</span>
</div>
<div class="legend-item" data-orbit="meo">
<span class="legend-dot" style="background:#fbbf24"></span>
<span class="legend-label">MEO</span>
<span class="legend-detail">2 000 35 786 km</span>
</div>
<div class="legend-item" data-orbit="geo">
<span class="legend-dot" style="background:#f87171"></span>
<span class="legend-label">GEO</span>
<span class="legend-detail">35 786 km (fixed)</span>
</div>
</div>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H;
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// ── Stars ──
const stars = Array.from({length: 180}, () => ({
x: Math.random(), y: Math.random(),
r: Math.random() * 1.2 + 0.2,
tw: Math.random() * Math.PI * 2,
sp: 0.3 + Math.random() * 1.5,
}));
// ── Orbit definitions ──
// Radii proportional: Earth radius = 6371 km
// Visual scale: earthR in pixels, then orbits are proportional
const earthRealKm = 6371;
const orbits = [
{
name: 'LEO', fullName: 'Low Earth Orbit',
color: '#34d399', colorFade: 'rgba(52,211,153,',
altitudeKm: 550, // typical Starlink
periodMin: 95, // ~95 min
numSats: 8,
latencyMs: '4 20',
coverage: 'Small footprint (~1000 km)',
examples: 'Starlink, OneWeb, Iridium',
fsplDb: '~155 dB (Ku)',
pros: 'Low latency, lower FSPL',
cons: 'Many sats needed, handover required',
speedFactor: 6.0, // relative orbital speed (fastest)
},
{
name: 'MEO', fullName: 'Medium Earth Orbit',
color: '#fbbf24', colorFade: 'rgba(251,191,36,',
altitudeKm: 20200, // GPS
periodMin: 720, // 12h
numSats: 5,
latencyMs: '40 80',
coverage: 'Mid footprint (~12 000 km)',
examples: 'GPS, Galileo, O3b/SES',
fsplDb: '~186 dB (Ku)',
pros: 'Good latency/coverage balance',
cons: 'Moderate constellation size',
speedFactor: 1.5,
},
{
name: 'GEO', fullName: 'Geostationary Orbit',
color: '#f87171', colorFade: 'rgba(248,113,113,',
altitudeKm: 35786,
periodMin: 1436, // 23h56
numSats: 3,
latencyMs: '240 280',
coverage: 'Huge footprint (~1/3 Earth)',
examples: 'Intelsat, SES, Eutelsat',
fsplDb: '~205 dB (Ku)',
pros: 'Fixed position, 3 sats = global',
cons: 'High latency, high FSPL',
speedFactor: 0.3,
},
];
// ── Satellite objects ──
let satellites = [];
function initSats() {
satellites = [];
orbits.forEach(o => {
for (let i = 0; i < o.numSats; i++) {
satellites.push({
orbit: o,
angle: (Math.PI * 2 * i) / o.numSats + Math.random() * 0.3,
hovered: false,
});
}
});
}
initSats();
let t = 0;
let hoveredOrbit = null;
let mouseX = -1, mouseY = -1;
canvas.addEventListener('mousemove', (e) => {
mouseX = e.offsetX;
mouseY = e.offsetY;
});
canvas.addEventListener('mouseleave', () => {
mouseX = mouseY = -1;
hoveredOrbit = null;
document.getElementById('infoContent').innerHTML =
'<p style="color:#94a3b8; text-align:center;">Hover over a satellite to see its details.</p>';
});
// Legend hover
document.querySelectorAll('.legend-item').forEach(el => {
el.addEventListener('mouseenter', () => {
const name = el.dataset.orbit.toUpperCase();
const o = orbits.find(o => o.name === name);
if (o) showInfo(o);
});
el.addEventListener('mouseleave', () => {
hoveredOrbit = null;
document.getElementById('infoContent').innerHTML =
'<p style="color:#94a3b8; text-align:center;">Hover over a satellite to see its details.</p>';
});
});
function showInfo(o) {
hoveredOrbit = o.name;
document.getElementById('infoContent').innerHTML = `
<div class="orbit-name" style="color:${o.color}">${o.name} — ${o.fullName}</div>
<hr>
<div class="stat"><span class="label">Altitude</span><span class="value">${o.altitudeKm.toLocaleString()} km</span></div>
<div class="stat"><span class="label">Period</span><span class="value">${o.periodMin >= 60 ? (o.periodMin/60).toFixed(1)+'h' : o.periodMin+'min'}</span></div>
<div class="stat"><span class="label">Latency (RTT)</span><span class="value">${o.latencyMs} ms</span></div>
<div class="stat"><span class="label">FSPL</span><span class="value">${o.fsplDb}</span></div>
<div class="stat"><span class="label">Coverage</span><span class="value">${o.coverage}</span></div>
<hr>
<div class="stat"><span class="label">✅ Pros</span><span class="value" style="text-align:right; max-width:160px">${o.pros}</span></div>
<div class="stat"><span class="label">⚠️ Cons</span><span class="value" style="text-align:right; max-width:160px">${o.cons}</span></div>
<hr>
<div class="stat"><span class="label">Examples</span><span class="value" style="color:#4FC3F7">${o.examples}</span></div>
`;
}
function getOrbitRadius(altKm) {
const earthR = Math.min(W, H) * 0.12;
const maxAlt = 42000;
const maxOrbitR = Math.min(W, H) * 0.44;
return earthR + (altKm / maxAlt) * (maxOrbitR - earthR);
}
function drawBackground() {
const bg = ctx.createRadialGradient(W/2, H/2, 0, W/2, H/2, W * 0.7);
bg.addColorStop(0, '#0a1628');
bg.addColorStop(1, '#020617');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
stars.forEach(s => {
const alpha = 0.3 + Math.sin(t * s.sp + s.tw) * 0.3 + 0.3;
ctx.beginPath();
ctx.arc(s.x * W, s.y * H, s.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,255,${alpha})`;
ctx.fill();
});
}
function drawEarth() {
const cx = W / 2;
const cy = H / 2;
const r = Math.min(W, H) * 0.12;
// Glow
const glow = ctx.createRadialGradient(cx, cy, r * 0.8, cx, cy, r * 1.6);
glow.addColorStop(0, 'rgba(56,189,248,0.1)');
glow.addColorStop(1, 'transparent');
ctx.fillStyle = glow;
ctx.fillRect(0, 0, W, H);
// Body
const grad = ctx.createRadialGradient(cx - r*0.3, cy - r*0.3, r*0.1, cx, cy, r);
grad.addColorStop(0, '#2dd4bf');
grad.addColorStop(0.3, '#0f766e');
grad.addColorStop(0.6, '#0e4f72');
grad.addColorStop(1, '#0c2d48');
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
// Atmosphere rim
ctx.beginPath();
ctx.arc(cx, cy, r + 3, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(56,189,248,0.3)';
ctx.lineWidth = 4;
ctx.stroke();
// Label
ctx.fillStyle = '#e2e8f0';
ctx.font = 'bold 13px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Earth', cx, cy + 4);
ctx.font = '10px system-ui';
ctx.fillStyle = '#64748b';
ctx.fillText('R = 6 371 km', cx, cy + 18);
ctx.textAlign = 'start';
return { cx, cy, r };
}
function drawOrbits(earth) {
orbits.forEach(o => {
const r = getOrbitRadius(o.altitudeKm);
const isHighlight = hoveredOrbit === o.name;
// Orbit path
ctx.beginPath();
ctx.arc(earth.cx, earth.cy, r, 0, Math.PI * 2);
ctx.strokeStyle = isHighlight
? o.color
: o.colorFade + '0.18)';
ctx.lineWidth = isHighlight ? 2.5 : 1;
ctx.setLineDash(o.name === 'GEO' ? [] : [6, 4]);
ctx.stroke();
ctx.setLineDash([]);
// Coverage cone (for highlighted orbit)
if (isHighlight) {
const coneAngle = o.name === 'GEO' ? 0.28 : o.name === 'MEO' ? 0.18 : 0.08;
ctx.beginPath();
ctx.moveTo(earth.cx, earth.cy);
// Draw cone from earth center to orbit
const sampleAngle = satellites.find(s => s.orbit.name === o.name)?.angle || 0;
const sx = earth.cx + Math.cos(sampleAngle) * r;
const sy = earth.cy + Math.sin(sampleAngle) * r;
ctx.moveTo(sx, sy);
ctx.lineTo(
sx + Math.cos(sampleAngle + Math.PI + coneAngle) * r * 0.7,
sy + Math.sin(sampleAngle + Math.PI + coneAngle) * r * 0.7,
);
ctx.lineTo(
sx + Math.cos(sampleAngle + Math.PI - coneAngle) * r * 0.7,
sy + Math.sin(sampleAngle + Math.PI - coneAngle) * r * 0.7,
);
ctx.closePath();
ctx.fillStyle = o.colorFade + '0.06)';
ctx.fill();
}
// Altitude label on orbit ring
const labelAngle = -Math.PI * 0.25;
const lx = earth.cx + Math.cos(labelAngle) * (r + 14);
const ly = earth.cy + Math.sin(labelAngle) * (r + 14);
ctx.fillStyle = isHighlight ? o.color : o.colorFade + '0.5)';
ctx.font = isHighlight ? 'bold 11px system-ui' : '10px system-ui';
ctx.textAlign = 'center';
ctx.fillText(
`${o.name} · ${o.altitudeKm >= 10000 ? (o.altitudeKm/1000).toFixed(0)+'k' : o.altitudeKm.toLocaleString()} km`,
lx, ly
);
ctx.textAlign = 'start';
});
}
function drawSatellites(earth) {
const speed = parseFloat(document.getElementById('speedSlider').value);
satellites.forEach(sat => {
const r = getOrbitRadius(sat.orbit.altitudeKm);
sat.angle += sat.orbit.speedFactor * speed * 0.001;
const sx = earth.cx + Math.cos(sat.angle) * r;
const sy = earth.cy + Math.sin(sat.angle) * r;
// Hit test
const dist = Math.sqrt((mouseX - sx)**2 + (mouseY - sy)**2);
sat.hovered = dist < 18;
if (sat.hovered) showInfo(sat.orbit);
const isHighlight = hoveredOrbit === sat.orbit.name;
const satSize = isHighlight ? 7 : 5;
// Satellite glow
if (isHighlight) {
ctx.beginPath();
ctx.arc(sx, sy, satSize + 8, 0, Math.PI * 2);
ctx.fillStyle = sat.orbit.colorFade + '0.15)';
ctx.fill();
}
// Satellite body
ctx.beginPath();
ctx.arc(sx, sy, satSize, 0, Math.PI * 2);
ctx.fillStyle = isHighlight ? sat.orbit.color : sat.orbit.colorFade + '0.7)';
ctx.fill();
// Solar panel lines
const panelLen = isHighlight ? 10 : 6;
const pAngle = sat.angle + Math.PI / 2;
ctx.beginPath();
ctx.moveTo(sx - Math.cos(pAngle) * panelLen, sy - Math.sin(pAngle) * panelLen);
ctx.lineTo(sx + Math.cos(pAngle) * panelLen, sy + Math.sin(pAngle) * panelLen);
ctx.strokeStyle = isHighlight ? sat.orbit.color : sat.orbit.colorFade + '0.4)';
ctx.lineWidth = isHighlight ? 2.5 : 1.5;
ctx.stroke();
// Signal line to Earth (when highlighted)
if (isHighlight) {
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(earth.cx, earth.cy);
ctx.strokeStyle = sat.orbit.colorFade + '0.12)';
ctx.lineWidth = 1;
ctx.setLineDash([3, 5]);
ctx.stroke();
ctx.setLineDash([]);
}
});
}
function drawLatencyComparison(earth) {
const barX = 46;
const barY = H * 0.08;
const barH = 18;
const maxMs = 300;
const gap = 6;
ctx.fillStyle = '#94a3b8';
ctx.font = 'bold 11px system-ui';
ctx.fillText('Round-Trip Latency', barX, barY - 10);
orbits.forEach((o, i) => {
const y = barY + i * (barH + gap);
const latAvg = o.name === 'LEO' ? 12 : o.name === 'MEO' ? 60 : 260;
const barW = (latAvg / maxMs) * 160;
// Bar bg
ctx.fillStyle = 'rgba(30,58,95,0.4)';
roundRect(ctx, barX, y, 160, barH, 4);
ctx.fill();
// Bar fill
const fillGrad = ctx.createLinearGradient(barX, 0, barX + barW, 0);
fillGrad.addColorStop(0, o.colorFade + '0.8)');
fillGrad.addColorStop(1, o.colorFade + '0.4)');
roundRect(ctx, barX, y, barW, barH, 4);
ctx.fillStyle = fillGrad;
ctx.fill();
// Label
ctx.fillStyle = '#e2e8f0';
ctx.font = '10px system-ui';
ctx.fillText(`${o.name} ${o.latencyMs} ms`, barX + 6, y + 13);
});
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
// ── Main loop ──
function draw() {
t += 0.016;
ctx.clearRect(0, 0, W, H);
drawBackground();
const earth = drawEarth();
drawOrbits(earth);
drawSatellites(earth);
drawLatencyComparison(earth);
// Title
ctx.fillStyle = '#e2e8f0';
ctx.font = 'bold 16px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Satellite Orbits — GEO vs MEO vs LEO', W/2, 30);
ctx.textAlign = 'start';
requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>
"""
def render():
"""Render the GEO/MEO/LEO orbit comparison animation."""
st.markdown("## 🌍 Satellite Orbits — GEO vs MEO vs LEO")
st.markdown(
"Compare the three main satellite orbit types. "
"Hover over satellites or legend items to explore their characteristics."
)
st.divider()
components.html(_ORBITS_HTML, height=680, scrolling=False)
# ── Educational content below ──
st.divider()
st.markdown("### 📊 Orbit Comparison at a Glance")
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("""
#### 🟢 LEO — Low Earth Orbit
**160 2 000 km**
- ⚡ Latency: **4 20 ms** (RTT)
- 📉 FSPL: ~155 dB (Ku-band)
- 🔄 Period: ~90 120 min
- 📡 Small footprint → **large constellations** needed (hundreds to thousands)
- 🤝 Requires **handover** between satellites
- 🛰️ *Starlink (550 km), OneWeb (1 200 km), Iridium (780 km)*
""")
with col2:
st.markdown("""
#### 🟡 MEO — Medium Earth Orbit
**2 000 35 786 km**
- ⚡ Latency: **40 80 ms** (RTT)
- 📉 FSPL: ~186 dB (Ku-band)
- 🔄 Period: ~2 12 h
- 📡 Medium footprint → **medium constellations** (20 50 sats)
- 🌍 Good balance between coverage and latency
- 🛰️ *GPS (20 200 km), Galileo, O3b/SES (8 000 km)*
""")
with col3:
st.markdown("""
#### 🔴 GEO — Geostationary Orbit
**35 786 km (fixed)**
- ⚡ Latency: **240 280 ms** (RTT)
- 📉 FSPL: ~205 dB (Ku-band)
- 🔄 Period: 23 h 56 min (= 1 sidereal day)
- 📡 Huge footprint → **3 sats = global** coverage
- 📌 **Fixed position** in the sky — no tracking needed
- 🛰️ *Intelsat, SES, Eutelsat, ViaSat*
""")
with st.expander("🔬 Key Trade-offs in Detail"):
st.markdown(r"""
| Parameter | LEO | MEO | GEO |
|:---|:---:|:---:|:---:|
| **Altitude** | 160 2 000 km | 2 000 35 786 km | 35 786 km |
| **Round-trip latency** | 4 20 ms | 40 80 ms | 240 280 ms |
| **Free-Space Path Loss** | ~155 dB | ~186 dB | ~205 dB |
| **Orbital period** | 90 120 min | 2 12 h | 23h 56m |
| **Coverage per sat** | ~1 000 km | ~12 000 km | ~15 000 km |
| **Constellation size** | Hundreds thousands | 20 50 | 3 |
| **Antenna tracking** | Required (fast) | Required (slow) | Fixed dish |
| **Doppler shift** | High | Moderate | Negligible |
| **Launch cost/sat** | Lower | Medium | Higher |
| **Orbital lifetime** | 5 7 years | 10 15 years | 15+ years |
**FSPL formula:** $\text{FSPL (dB)} = 20 \log_{10}(d) + 20 \log_{10}(f) + 32.44$
Where $d$ is distance in km and $f$ is frequency in MHz.
**Why it matters for Shannon:**
The higher the orbit, the greater the FSPL, the lower the received C/N.
Since $C = B \log_2(1 + C/N)$, a higher orbit means lower achievable bit rate
for the same bandwidth and transmit power. LEO compensates with lower FSPL
but requires more satellites and complex handover.
""")
with st.expander("🛰️ Notable Constellations"):
st.markdown("""
| Constellation | Orbit | Altitude | # Satellites | Use Case |
|:---|:---:|:---:|:---:|:---|
| **Starlink** | LEO | 550 km | ~6 000+ | Broadband Internet |
| **OneWeb** | LEO | 1 200 km | ~650 | Enterprise connectivity |
| **Iridium NEXT** | LEO | 780 km | 66 + spares | Global voice/data |
| **O3b mPOWER** | MEO | 8 000 km | 11+ | Managed connectivity |
| **GPS** | MEO | 20 200 km | 31 | Navigation |
| **Galileo** | MEO | 23 222 km | 30 | Navigation |
| **Intelsat** | GEO | 35 786 km | 50+ | Video & enterprise |
| **SES** | GEO + MEO | Mixed | 70+ | Video & data |
| **Eutelsat** | GEO | 35 786 km | 35+ | Video broadcasting |
""")

412
views/real_world.py Normal file
View File

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

View File

@@ -0,0 +1,762 @@
"""
Interactive Satellite Link Animation
=====================================
HTML5 Canvas animation showing a satellite communicating with a ground station,
with toggleable impairment layers (atmosphere, rain, free-space loss, noise…).
"""
import streamlit as st
import streamlit.components.v1 as components
_ANIMATION_HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: transparent; overflow: hidden; font-family: 'Segoe UI', system-ui, sans-serif; }
canvas { display: block; }
/* ── Control Panel ── */
#controls {
position: absolute; top: 14px; right: 18px;
background: rgba(13,27,42,0.92); border: 1px solid rgba(79,195,247,0.3);
border-radius: 14px; padding: 16px 18px; min-width: 220px;
backdrop-filter: blur(12px); box-shadow: 0 8px 32px rgba(0,0,0,0.45);
}
#controls h3 {
color: #4FC3F7; font-size: 13px; text-transform: uppercase;
letter-spacing: 1.5px; margin-bottom: 12px; text-align: center;
}
.toggle-row {
display: flex; align-items: center; justify-content: space-between;
margin: 7px 0; cursor: pointer; padding: 4px 6px; border-radius: 8px;
transition: background 0.2s;
}
.toggle-row:hover { background: rgba(79,195,247,0.08); }
.toggle-row label { color: #cbd5e1; font-size: 12.5px; cursor: pointer; flex: 1; }
.toggle-row .dot {
width: 10px; height: 10px; border-radius: 50%; margin-right: 10px; flex-shrink: 0;
}
/* Toggle switch */
.switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute; cursor: pointer; inset: 0;
background: #334155; border-radius: 20px; transition: 0.3s;
}
.slider::before {
content: ""; position: absolute; height: 14px; width: 14px;
left: 3px; bottom: 3px; background: #94a3b8;
border-radius: 50%; transition: 0.3s;
}
.switch input:checked + .slider { background: #0f3460; }
.switch input:checked + .slider::before { transform: translateX(16px); background: #4FC3F7; }
/* Legend box */
#legend {
position: absolute; bottom: 14px; left: 18px;
background: rgba(13,27,42,0.88); border: 1px solid rgba(79,195,247,0.2);
border-radius: 12px; padding: 14px 16px; max-width: 340px;
backdrop-filter: blur(10px); color: #e2e8f0; font-size: 12px; line-height: 1.6;
box-shadow: 0 4px 20px rgba(0,0,0,0.35);
transition: opacity 0.4s;
}
#legend h4 { color: #4FC3F7; margin-bottom: 6px; font-size: 13px; }
#legend .info-text { color: #94a3b8; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<!-- Control toggles -->
<div id="controls">
<h3>⚡ Impairments</h3>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#ff6b6b"></span>
<label>Free-Space Loss</label>
<label class="switch"><input type="checkbox" id="tFSL" checked><span class="slider"></span></label>
</div>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#ffd93d"></span>
<label>Atmospheric Attn.</label>
<label class="switch"><input type="checkbox" id="tAtm" checked><span class="slider"></span></label>
</div>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#6bcbff"></span>
<label>Rain Attenuation</label>
<label class="switch"><input type="checkbox" id="tRain" checked><span class="slider"></span></label>
</div>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#c084fc"></span>
<label>Ionospheric Effects</label>
<label class="switch"><input type="checkbox" id="tIono" checked><span class="slider"></span></label>
</div>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#fb923c"></span>
<label>Thermal Noise</label>
<label class="switch"><input type="checkbox" id="tNoise" checked><span class="slider"></span></label>
</div>
<div class="toggle-row" onclick="this.querySelector('input').click()">
<span class="dot" style="background:#34d399"></span>
<label>Pointing Loss</label>
<label class="switch"><input type="checkbox" id="tPoint" checked><span class="slider"></span></label>
</div>
</div>
<!-- Info legend -->
<div id="legend">
<h4 id="legendTitle">📡 Satellite Link Overview</h4>
<span id="legendText" class="info-text">
Toggle each impairment to see how it affects the signal.
<b style="color:#4FC3F7">Cyan</b> = Downlink (sat → ground).
<b style="color:#34d399">Green</b> = Uplink (ground → sat).
The signal strength bar shows the cumulative effect.
</span>
</div>
<script>
// ── Canvas setup ──
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H;
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// ── Toggle states ──
const ids = ['tFSL','tAtm','tRain','tIono','tNoise','tPoint'];
function isOn(id) { return document.getElementById(id).checked; }
// Legend descriptions
const legendData = {
tFSL: { title: '📉 Free-Space Path Loss (FSPL)',
text: 'Signal power decreases with the square of distance (1/r²). At 36,000 km (GEO), FSPL ≈ 200 dB at Ku-band. This is the dominant loss in any satellite link.' },
tAtm: { title: '🌫️ Atmospheric Attenuation',
text: 'Oxygen and water vapour molecules absorb RF energy. Attenuation increases with frequency: ~0.2 dB at C-band to >1 dB at Ka-band for a 30° elevation.' },
tRain: { title: '🌧️ Rain Attenuation',
text: 'Raindrops scatter and absorb signals, especially above 10 GHz. A heavy storm can add 1020 dB of loss at Ka-band. ITU-R P.618 models this statistically.' },
tIono: { title: '🔮 Ionospheric Effects',
text: 'The ionosphere causes Faraday rotation, scintillation and group delay. Effects are strongest below 3 GHz and vary with solar activity (11-year cycle).' },
tNoise: { title: '🌡️ Thermal Noise',
text: 'Every component adds noise (N = kTB). The system noise temperature combines antenna noise, LNB noise figure, and sky temperature. Lower C/N = fewer bits/s.' },
tPoint: { title: '🎯 Pointing Loss',
text: 'Misalignment between the antenna boresight and satellite direction. A 0.1° error on a 1.2 m dish at Ku-band ≈ 0.5 dB loss. Wind and thermal expansion are causes.' },
};
// Hover / click detection on toggles
ids.forEach(id => {
const row = document.getElementById(id).closest('.toggle-row');
row.addEventListener('mouseenter', () => {
const d = legendData[id];
document.getElementById('legendTitle').textContent = d.title;
document.getElementById('legendText').textContent = d.text;
});
row.addEventListener('mouseleave', () => {
document.getElementById('legendTitle').textContent = '📡 Satellite Link Overview';
document.getElementById('legendText').innerHTML = 'Toggle each impairment to see how it affects the signal. <b style=\"color:#4FC3F7\">Cyan</b> = Downlink (sat → ground). <b style=\"color:#34d399\">Green</b> = Uplink (ground → sat). The signal strength bar shows the cumulative effect.';
});
});
// ── Stars ──
const stars = Array.from({length: 200}, () => ({
x: Math.random(), y: Math.random() * 0.65,
r: Math.random() * 1.3 + 0.3,
twinkle: Math.random() * Math.PI * 2,
speed: 0.3 + Math.random() * 1.5,
}));
// ── Rain drops ──
const drops = Array.from({length: 120}, () => ({
x: Math.random(), y: Math.random(),
len: 8 + Math.random() * 18,
speed: 4 + Math.random() * 6,
opacity: 0.15 + Math.random() * 0.35,
}));
// ── Animation state ──
let t = 0;
const satOrbitAngle = { v: 0 };
// ── Signal packets (fixed pool, bidirectional) ──
const MAX_DOWN = 8; // downlink packets (sat → ground)
const MAX_UP = 5; // uplink packets (ground → sat)
let downPackets = [];
let upPackets = [];
function initPackets() {
downPackets = Array.from({length: MAX_DOWN}, (_, i) => ({
progress: i / MAX_DOWN, // spread evenly
speed: 0.003 + Math.random() * 0.002,
}));
upPackets = Array.from({length: MAX_UP}, (_, i) => ({
progress: i / MAX_UP,
speed: 0.0025 + Math.random() * 0.002,
}));
}
initPackets();
// ── Drawing helpers ──
function lerp(a, b, t) { return a + (b - a) * t; }
function drawEarth() {
const earthY = H * 0.88;
const earthR = W * 0.9;
// Atmosphere glow
const atmGrad = ctx.createRadialGradient(W/2, earthY + earthR*0.3, earthR * 0.85, W/2, earthY + earthR*0.3, earthR * 1.15);
atmGrad.addColorStop(0, 'rgba(56,189,248,0.08)');
atmGrad.addColorStop(0.5, 'rgba(56,189,248,0.03)');
atmGrad.addColorStop(1, 'transparent');
ctx.fillStyle = atmGrad;
ctx.fillRect(0, 0, W, H);
// Earth body
const grad = ctx.createRadialGradient(W/2, earthY + earthR*0.3, earthR * 0.2, W/2, earthY + earthR*0.3, earthR);
grad.addColorStop(0, '#1a6b4a');
grad.addColorStop(0.4, '#0f4c75');
grad.addColorStop(0.8, '#0b3d6b');
grad.addColorStop(1, '#062a4d');
ctx.beginPath();
ctx.arc(W/2, earthY + earthR * 0.3, earthR, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
// Atmosphere rim
if (isOn('tAtm')) {
ctx.beginPath();
ctx.arc(W/2, earthY + earthR * 0.3, earthR + 8, Math.PI, 2 * Math.PI);
ctx.strokeStyle = 'rgba(56,189,248,0.25)';
ctx.lineWidth = 18;
ctx.stroke();
ctx.beginPath();
ctx.arc(W/2, earthY + earthR * 0.3, earthR + 25, Math.PI * 1.1, Math.PI * 1.9);
ctx.strokeStyle = 'rgba(255,217,61,0.12)';
ctx.lineWidth = 12;
ctx.stroke();
// Label
ctx.fillStyle = 'rgba(255,217,61,0.7)';
ctx.font = '11px system-ui';
ctx.fillText('Troposphere', W/2 + earthR * 0.25, earthY - 30);
}
// Ionosphere
if (isOn('tIono')) {
for (let i = 0; i < 3; i++) {
ctx.beginPath();
ctx.arc(W/2, earthY + earthR * 0.3, earthR + 55 + i * 18, Math.PI * 1.05, Math.PI * 1.95);
ctx.strokeStyle = `rgba(192,132,252,${0.08 + Math.sin(t * 0.8 + i) * 0.05})`;
ctx.lineWidth = 2;
ctx.stroke();
}
ctx.fillStyle = 'rgba(192,132,252,0.6)';
ctx.font = '11px system-ui';
ctx.fillText('Ionosphere', W/2 - earthR * 0.35, earthY - 85);
}
return earthY;
}
function drawGroundStation(earthY) {
const gx = W * 0.38;
const gy = earthY - 18;
// Dish base
ctx.fillStyle = '#475569';
ctx.fillRect(gx - 4, gy - 5, 8, 20);
// Dish
ctx.beginPath();
ctx.ellipse(gx, gy - 12, 22, 28, -0.4, -Math.PI * 0.5, Math.PI * 0.5);
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 3;
ctx.stroke();
// Feed
ctx.beginPath();
ctx.moveTo(gx, gy - 12);
ctx.lineTo(gx + 18, gy - 30);
ctx.strokeStyle = '#64748b';
ctx.lineWidth = 2;
ctx.stroke();
// LNB
ctx.beginPath();
ctx.arc(gx + 18, gy - 31, 4, 0, Math.PI * 2);
ctx.fillStyle = '#f59e0b';
ctx.fill();
// Label
ctx.fillStyle = '#94a3b8';
ctx.font = 'bold 11px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Ground Station', gx, gy + 28);
ctx.textAlign = 'start';
return { x: gx + 18, y: gy - 31 };
}
function drawSatellite() {
const cx = W * 0.62;
const cy = H * 0.12;
const bob = Math.sin(t * 0.5) * 4;
const sx = cx;
const sy = cy + bob;
// Solar panels
ctx.fillStyle = '#1e3a5f';
ctx.strokeStyle = '#38bdf8';
ctx.lineWidth = 1;
for (const side of [-1, 1]) {
ctx.save();
ctx.translate(sx + side * 22, sy);
ctx.fillRect(side > 0 ? 4 : -38, -14, 34, 28);
ctx.strokeRect(side > 0 ? 4 : -38, -14, 34, 28);
// Panel lines
for (let i = 1; i < 4; i++) {
ctx.beginPath();
ctx.moveTo(side > 0 ? 4 + i * 8.5 : -38 + i * 8.5, -14);
ctx.lineTo(side > 0 ? 4 + i * 8.5 : -38 + i * 8.5, 14);
ctx.strokeStyle = 'rgba(56,189,248,0.3)';
ctx.stroke();
}
ctx.restore();
}
// Body
ctx.fillStyle = '#334155';
ctx.strokeStyle = '#64748b';
ctx.lineWidth = 1.5;
ctx.fillRect(sx - 12, sy - 16, 24, 32);
ctx.strokeRect(sx - 12, sy - 16, 24, 32);
// Antenna dish
ctx.beginPath();
ctx.ellipse(sx, sy + 22, 10, 5, 0, 0, Math.PI);
ctx.fillStyle = '#94a3b8';
ctx.fill();
// Antenna feed
ctx.beginPath();
ctx.moveTo(sx, sy + 16);
ctx.lineTo(sx, sy + 28);
ctx.strokeStyle = '#cbd5e1';
ctx.lineWidth = 2;
ctx.stroke();
// Status LED
ctx.beginPath();
ctx.arc(sx, sy - 10, 3, 0, Math.PI * 2);
ctx.fillStyle = `rgba(52,211,153,${0.5 + Math.sin(t * 2) * 0.5})`;
ctx.fill();
// Label
ctx.fillStyle = '#94a3b8';
ctx.font = 'bold 11px system-ui';
ctx.textAlign = 'center';
ctx.fillText('GEO Satellite', sx, sy - 34);
ctx.font = '10px system-ui';
ctx.fillStyle = '#64748b';
ctx.fillText('36 000 km', sx, sy - 22);
ctx.textAlign = 'start';
return { x: sx, y: sy + 28 };
}
function drawSignalBeam(from, to) {
// Count active impairments
const active = ids.filter(isOn).length;
const strength = Math.max(0.15, 1 - active * 0.12);
const dx = to.x - from.x;
const dy = to.y - from.y;
const len = Math.sqrt(dx*dx + dy*dy);
const nx = -dy / len;
const ny = dx / len;
const spreadTop = 8;
const spreadBot = 45;
// ── Downlink beam cone (sat → ground) ──
const beamGradDown = ctx.createLinearGradient(from.x, from.y, to.x, to.y);
beamGradDown.addColorStop(0, `rgba(79,195,247,${0.20 * strength})`);
beamGradDown.addColorStop(0.5, `rgba(79,195,247,${0.08 * strength})`);
beamGradDown.addColorStop(1, `rgba(79,195,247,${0.03 * strength})`);
ctx.beginPath();
ctx.moveTo(from.x + nx * spreadTop, from.y + ny * spreadTop);
ctx.lineTo(to.x + nx * spreadBot, to.y + ny * spreadBot);
ctx.lineTo(to.x - nx * spreadBot * 0.3, to.y - ny * spreadBot * 0.3);
ctx.lineTo(from.x - nx * spreadTop * 0.3, from.y - ny * spreadTop * 0.3);
ctx.closePath();
ctx.fillStyle = beamGradDown;
ctx.fill();
// ── Uplink beam cone (ground → sat) — offset, different color ──
const beamGradUp = ctx.createLinearGradient(to.x, to.y, from.x, from.y);
beamGradUp.addColorStop(0, `rgba(52,211,153,${0.14 * strength})`);
beamGradUp.addColorStop(0.5, `rgba(52,211,153,${0.06 * strength})`);
beamGradUp.addColorStop(1, `rgba(52,211,153,${0.02 * strength})`);
const offX = nx * 20; // lateral offset so beams don't overlap
const offY = ny * 20;
ctx.beginPath();
ctx.moveTo(from.x - nx * spreadTop + offX, from.y - ny * spreadTop + offY);
ctx.lineTo(to.x - nx * spreadBot * 0.6 + offX, to.y - ny * spreadBot * 0.6 + offY);
ctx.lineTo(to.x + nx * spreadBot * 0.1 + offX, to.y + ny * spreadBot * 0.1 + offY);
ctx.lineTo(from.x + nx * spreadTop * 0.1 + offX, from.y + ny * spreadTop * 0.1 + offY);
ctx.closePath();
ctx.fillStyle = beamGradUp;
ctx.fill();
// ── Downlink packets (sat → ground) — cyan ──
downPackets.forEach(p => {
p.progress += p.speed;
if (p.progress > 1) p.progress -= 1; // wrap around, never accumulate
const px = lerp(from.x, to.x, p.progress);
const py = lerp(from.y, to.y, p.progress);
const sz = 2.5 + Math.sin(p.progress * Math.PI) * 3;
const alpha = Math.sin(p.progress * Math.PI) * 0.8 * strength;
ctx.beginPath();
ctx.arc(px, py, sz, 0, Math.PI * 2);
ctx.fillStyle = `rgba(79,195,247,${alpha})`;
ctx.fill();
ctx.beginPath();
ctx.arc(px, py, sz + 4, 0, Math.PI * 2);
ctx.fillStyle = `rgba(79,195,247,${alpha * 0.25})`;
ctx.fill();
});
// ── Uplink packets (ground → sat) — green, offset path ──
upPackets.forEach(p => {
p.progress += p.speed;
if (p.progress > 1) p.progress -= 1;
const px = lerp(to.x, from.x, p.progress) + offX;
const py = lerp(to.y, from.y, p.progress) + offY;
const sz = 2 + Math.sin(p.progress * Math.PI) * 2.5;
const alpha = Math.sin(p.progress * Math.PI) * 0.7 * strength;
ctx.beginPath();
ctx.arc(px, py, sz, 0, Math.PI * 2);
ctx.fillStyle = `rgba(52,211,153,${alpha})`;
ctx.fill();
ctx.beginPath();
ctx.arc(px, py, sz + 3, 0, Math.PI * 2);
ctx.fillStyle = `rgba(52,211,153,${alpha * 0.25})`;
ctx.fill();
});
// ── Beam labels ──
const midX = lerp(from.x, to.x, 0.12);
const midY = lerp(from.y, to.y, 0.12);
ctx.font = '10px system-ui';
ctx.fillStyle = 'rgba(79,195,247,0.7)';
ctx.fillText('▼ Downlink', midX + 12, midY);
ctx.fillStyle = 'rgba(52,211,153,0.7)';
ctx.fillText('▲ Uplink', midX + offX - 50, midY + offY);
// ── Impairment markers along the beam ──
const impairments = [
{ id: 'tFSL', pos: 0.25, color: '#ff6b6b', label: 'FSPL 200 dB', symbol: '📉' },
{ id: 'tIono', pos: 0.55, color: '#c084fc', label: 'Iono scintillation', symbol: '🔮' },
{ id: 'tAtm', pos: 0.72, color: '#ffd93d', label: 'Gas absorption', symbol: '🌫️' },
{ id: 'tRain', pos: 0.82, color: '#6bcbff', label: 'Rain fade', symbol: '🌧️' },
{ id: 'tNoise', pos: 0.92, color: '#fb923c', label: 'N = kTB', symbol: '🌡️' },
{ id: 'tPoint', pos: 0.96, color: '#34d399', label: 'Pointing err.', symbol: '🎯' },
];
impairments.forEach(imp => {
if (!isOn(imp.id)) return;
const ix = lerp(from.x, to.x, imp.pos);
const iy = lerp(from.y, to.y, imp.pos);
// Pulse ring
const pulse = Math.sin(t * 2 + imp.pos * 10) * 0.3 + 0.7;
ctx.beginPath();
ctx.arc(ix, iy, 14 * pulse, 0, Math.PI * 2);
ctx.strokeStyle = imp.color;
ctx.lineWidth = 1.5;
ctx.globalAlpha = 0.6;
ctx.stroke();
ctx.globalAlpha = 1;
// Cross mark
const cs = 5;
ctx.beginPath();
ctx.moveTo(ix - cs, iy - cs); ctx.lineTo(ix + cs, iy + cs);
ctx.moveTo(ix + cs, iy - cs); ctx.lineTo(ix - cs, iy + cs);
ctx.strokeStyle = imp.color;
ctx.lineWidth = 2;
ctx.stroke();
// Label
ctx.fillStyle = imp.color;
ctx.font = '10px system-ui';
const labelX = ix + (imp.pos < 0.5 ? 20 : -ctx.measureText(imp.label).width - 20);
ctx.fillText(imp.label, labelX, iy + 4);
});
return strength;
}
function drawSignalBar(strength) {
const barX = 18;
const barY = H * 0.15;
const barW = 12;
const barH = H * 0.45;
// Background
ctx.fillStyle = 'rgba(30,58,95,0.5)';
ctx.strokeStyle = 'rgba(79,195,247,0.3)';
ctx.lineWidth = 1;
roundRect(ctx, barX, barY, barW, barH, 6);
ctx.fill();
ctx.stroke();
// Fill
const fillH = barH * strength;
const fillGrad = ctx.createLinearGradient(0, barY + barH - fillH, 0, barY + barH);
if (strength > 0.6) {
fillGrad.addColorStop(0, '#34d399');
fillGrad.addColorStop(1, '#059669');
} else if (strength > 0.3) {
fillGrad.addColorStop(0, '#fbbf24');
fillGrad.addColorStop(1, '#d97706');
} else {
fillGrad.addColorStop(0, '#f87171');
fillGrad.addColorStop(1, '#dc2626');
}
roundRect(ctx, barX + 1, barY + barH - fillH, barW - 2, fillH, 5);
ctx.fillStyle = fillGrad;
ctx.fill();
// Label
ctx.save();
ctx.translate(barX + barW + 6, barY + barH / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillStyle = '#64748b';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Signal Strength', 0, 0);
ctx.restore();
// Percentage
ctx.fillStyle = '#e2e8f0';
ctx.font = 'bold 12px system-ui';
ctx.textAlign = 'center';
ctx.fillText(Math.round(strength * 100) + '%', barX + barW/2, barY - 8);
ctx.textAlign = 'start';
}
function drawRain(earthY) {
if (!isOn('tRain')) return;
const rainZoneTop = earthY - 140;
const rainZoneBot = earthY - 10;
// Cloud
const cloudAlpha = 0.25 + Math.sin(t * 0.3) * 0.08;
ctx.fillStyle = `rgba(148,163,184,${cloudAlpha})`;
drawCloud(W * 0.42, rainZoneTop - 5, 60, 25);
drawCloud(W * 0.55, rainZoneTop + 10, 45, 20);
// Drops
drops.forEach(d => {
const dx = d.x * W * 0.5 + W * 0.25;
d.y += d.speed / H;
if (d.y > 1) { d.y = 0; d.x = Math.random(); }
const dy = rainZoneTop + d.y * (rainZoneBot - rainZoneTop);
ctx.beginPath();
ctx.moveTo(dx, dy);
ctx.lineTo(dx - 0.5, dy + d.len);
ctx.strokeStyle = `rgba(107,203,255,${d.opacity})`;
ctx.lineWidth = 1;
ctx.stroke();
});
}
function drawCloud(cx, cy, w, h) {
ctx.beginPath();
ctx.ellipse(cx, cy, w, h, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx - w * 0.45, cy + 5, w * 0.55, h * 0.7, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx + w * 0.4, cy + 3, w * 0.5, h * 0.65, 0, 0, Math.PI * 2);
ctx.fill();
}
function drawNoise(gndPos) {
if (!isOn('tNoise')) return;
for (let i = 0; i < 25; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = 15 + Math.random() * 30;
const nx = gndPos.x + Math.cos(angle) * dist;
const ny = gndPos.y + Math.sin(angle) * dist;
ctx.beginPath();
ctx.arc(nx, ny, 1 + Math.random() * 1.5, 0, Math.PI * 2);
ctx.fillStyle = `rgba(251,146,60,${0.2 + Math.random() * 0.4})`;
ctx.fill();
}
}
function drawPointingError(satPos, gndPos) {
if (!isOn('tPoint')) return;
const offset = Math.sin(t * 1.2) * 12;
ctx.setLineDash([4, 6]);
ctx.beginPath();
ctx.moveTo(gndPos.x, gndPos.y);
ctx.lineTo(gndPos.x + offset * 3, gndPos.y - 60);
ctx.strokeStyle = 'rgba(52,211,153,0.35)';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.setLineDash([]);
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
// ── Main loop ──
function draw() {
t += 0.016;
ctx.clearRect(0, 0, W, H);
// Background gradient (space)
const bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, '#020617');
bg.addColorStop(0.6, '#0a1628');
bg.addColorStop(1, '#0d1b2a');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
// Stars
stars.forEach(s => {
const alpha = 0.3 + Math.sin(t * s.speed + s.twinkle) * 0.35 + 0.35;
ctx.beginPath();
ctx.arc(s.x * W, s.y * H, s.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,255,${alpha})`;
ctx.fill();
});
// Earth
const earthY = drawEarth();
// Ground station
const gndPos = drawGroundStation(earthY);
// Satellite
const satPos = drawSatellite();
// Rain (behind beam)
drawRain(earthY);
// Signal beam
const strength = drawSignalBeam(satPos, gndPos);
// Noise sparkles
drawNoise(gndPos);
// Pointing error
drawPointingError(satPos, gndPos);
// Signal bar
drawSignalBar(strength);
// Title
ctx.fillStyle = '#e2e8f0';
ctx.font = 'bold 16px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Satellite Communication Link — Signal Impairments', W/2, 30);
ctx.textAlign = 'start';
requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>
"""
def render():
"""Render the satellite link animation page."""
st.markdown("## 🛰️ Satellite Link — Interactive Animation")
st.markdown(
"Visualise how a signal travels from a **GEO satellite** to a "
"**ground station** and discover the impairments that degrade it. "
"Toggle each effect on/off to see the impact on signal strength."
)
st.divider()
components.html(_ANIMATION_HTML, height=680, scrolling=False)
# ── Pedagogical summary below the animation ──
st.divider()
st.markdown("### 📚 Understanding the Link Budget")
col1, col2 = st.columns(2)
with col1:
st.markdown("""
**Uplink & Downlink path:**
The signal travels ~36 000 km from a geostationary satellite to Earth.
Along the way it encounters multiple sources of degradation:
1. **Free-Space Path Loss** — the dominant factor, purely geometric (1/r²)
2. **Atmospheric gases** — O₂ and H₂O absorption (ITU-R P.676)
3. **Rain** — scattering & absorption, worst at Ka-band (ITU-R P.618)
4. **Ionosphere** — Faraday rotation, scintillation (ITU-R P.531)
""")
with col2:
st.markdown("""
**At the receiver:**
Even after the signal arrives, further degradation occurs:
5. **Thermal noise** — every component adds noise: $N = k \\cdot T_{sys} \\cdot B$
6. **Pointing loss** — antenna misalignment reduces gain
7. **Implementation losses** — ADC quantisation, filter roll-off, etc.
The **Shannon limit** $C = B \\log_2(1 + C/N)$ tells us the maximum
bit rate achievable given the remaining signal-to-noise ratio.
""")
with st.expander("🔗 Key ITU-R Recommendations"):
st.markdown("""
| Recommendation | Topic |
|:---:|:---|
| **P.618** | Rain attenuation & propagation effects for satellite links |
| **P.676** | Gaseous attenuation on terrestrial and slant paths |
| **P.531** | Ionospheric effects on radiowave propagation |
| **P.837** | Rainfall rate statistics for prediction models |
| **P.839** | Rain height model for prediction methods |
| **S.1428** | Reference satellite link for system design |
""")

917
views/satellite_types.py Normal file
View File

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

269
views/theory.py Normal file
View File

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