Compare commits
No commits in common. "main" and "remove-python" have entirely different histories.
main
...
remove-pyt
@ -1,62 +0,0 @@
|
|||||||
# Rust build artifacts
|
|
||||||
target/
|
|
||||||
**/*.rs.bk
|
|
||||||
Cargo.lock # Let Docker resolve dependencies fresh
|
|
||||||
|
|
||||||
# Node.js
|
|
||||||
web/node_modules/
|
|
||||||
web/dist/
|
|
||||||
web/.vite/
|
|
||||||
|
|
||||||
# Git
|
|
||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs/
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Temporary files
|
|
||||||
*.tmp
|
|
||||||
*.temp
|
|
||||||
|
|
||||||
# Development files
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# Documentation (can be excluded for production)
|
|
||||||
README.md
|
|
||||||
DEPLOYMENT.md
|
|
||||||
STATUS.md
|
|
||||||
*.md
|
|
||||||
|
|
||||||
# Test files
|
|
||||||
test_*
|
|
||||||
*_test.rs
|
|
||||||
|
|
||||||
# Python cache
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
|
|
||||||
# Binary outputs
|
|
||||||
*.bin
|
|
||||||
trajectory.bin
|
|
||||||
|
|
||||||
# Development scripts (optional)
|
|
||||||
start_interfaces.sh
|
|
||||||
test_interfaces.sh
|
|
||||||
|
|
||||||
# Tauri (not needed for Docker web deployment)
|
|
||||||
src-tauri/
|
|
70
.gitignore
vendored
70
.gitignore
vendored
@ -1,73 +1,7 @@
|
|||||||
# Python cache and virtual environments
|
|
||||||
last_checkpoint.npz
|
last_checkpoint.npz
|
||||||
*.pyc
|
*.pyc
|
||||||
__pycache__/
|
__pycache__
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
env/
|
|
||||||
.env
|
|
||||||
*.egg-info/
|
|
||||||
|
|
||||||
# Build directories
|
|
||||||
/build
|
/build
|
||||||
/cmake-build-*
|
/cmake-build-*
|
||||||
CMakeFiles/
|
CMakeFiles/
|
||||||
*.cmake
|
*.cmake
|
||||||
|
|
||||||
# Rust build artifacts
|
|
||||||
/target/
|
|
||||||
/rust/target/
|
|
||||||
rust/Cargo.lock
|
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
# Simulation output files
|
|
||||||
out.out
|
|
||||||
output.json
|
|
||||||
*.bin
|
|
||||||
*.png
|
|
||||||
*.mp4
|
|
||||||
*.avi
|
|
||||||
traj.bin
|
|
||||||
trajectory.bin
|
|
||||||
test_output.bin
|
|
||||||
|
|
||||||
# Web frontend
|
|
||||||
web/node_modules/
|
|
||||||
web/dist/
|
|
||||||
web/.vite/
|
|
||||||
web/coverage/
|
|
||||||
web/build/
|
|
||||||
|
|
||||||
# Tauri desktop GUI
|
|
||||||
src-tauri/target/
|
|
||||||
src-tauri/gen/
|
|
||||||
|
|
||||||
# IDE and editor files
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Runtime and temporary files
|
|
||||||
.tmp/
|
|
||||||
.temp/
|
|
||||||
*.tmp
|
|
||||||
*.temp
|
|
||||||
|
|
||||||
# OS generated files
|
|
||||||
.DS_Store
|
|
||||||
.DS_Store?
|
|
||||||
._*
|
|
||||||
.Spotlight-V100
|
|
||||||
.Trashes
|
|
||||||
ehthumbs.db
|
|
||||||
Thumbs.db
|
|
49
CMakeLists.txt
Normal file
49
CMakeLists.txt
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(orbital_simulator)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
# Find required packages
|
||||||
|
find_package(Boost REQUIRED COMPONENTS program_options)
|
||||||
|
find_package(nlohmann_json REQUIRED)
|
||||||
|
|
||||||
|
# Add source files
|
||||||
|
set(SOURCES
|
||||||
|
src/main.cpp
|
||||||
|
src/body.cpp
|
||||||
|
src/calc.cpp
|
||||||
|
src/simulator.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add header files
|
||||||
|
set(HEADERS
|
||||||
|
src/body.hpp
|
||||||
|
src/calc.hpp
|
||||||
|
src/simulator.hpp
|
||||||
|
src/units.hpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create executable
|
||||||
|
add_executable(orbital_simulator ${SOURCES} ${HEADERS})
|
||||||
|
|
||||||
|
# Link libraries
|
||||||
|
target_link_libraries(orbital_simulator
|
||||||
|
PRIVATE
|
||||||
|
Boost::program_options
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include directories
|
||||||
|
target_include_directories(orbital_simulator
|
||||||
|
PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optional: Enable ncurses for terminal plotting
|
||||||
|
option(ENABLE_NCURSES "Enable terminal plotting with ncurses" OFF)
|
||||||
|
if(ENABLE_NCURSES)
|
||||||
|
find_package(Curses REQUIRED)
|
||||||
|
target_compile_definitions(orbital_simulator PRIVATE NCURSES_ENABLED)
|
||||||
|
target_link_libraries(orbital_simulator PRIVATE ${CURSES_LIBRARIES})
|
||||||
|
endif()
|
39
Cargo.toml
39
Cargo.toml
@ -1,39 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "orbital_simulator"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
authors = ["thomas"]
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
gui = ["dep:tauri", "dep:tauri-build"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bincode = "1.3"
|
|
||||||
clap = { version = "4.5.39", features = ["derive"] }
|
|
||||||
env_logger = "0.11.8"
|
|
||||||
glam = { version = "0.30.4", features = ["serde"] }
|
|
||||||
humantime = "2.2.0"
|
|
||||||
indicatif = "0.17.11"
|
|
||||||
log = "0.4.27"
|
|
||||||
once_cell = "1.21.3"
|
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
|
||||||
serde_json = "1.0.140"
|
|
||||||
serde_toml = "0.0.1"
|
|
||||||
toml = "0.8.23"
|
|
||||||
|
|
||||||
# Web API dependencies
|
|
||||||
axum = "0.7"
|
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
|
||||||
tower = "0.4"
|
|
||||||
tower-http = { version = "0.5", features = ["cors", "fs"] }
|
|
||||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
|
||||||
dashmap = "5.5"
|
|
||||||
|
|
||||||
# GUI dependencies (optional)
|
|
||||||
tauri = { version = "1.0", features = ["api-all"], optional = true }
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
tauri-build = { version = "1.0", optional = true }
|
|
302
DEPLOYMENT.md
302
DEPLOYMENT.md
@ -1,302 +0,0 @@
|
|||||||
# Production Deployment Guide
|
|
||||||
|
|
||||||
This guide explains how to build and deploy the Orbital Simulator in production environments.
|
|
||||||
|
|
||||||
## Building for Production
|
|
||||||
|
|
||||||
### 1. Rust API Server
|
|
||||||
|
|
||||||
Build the optimized API server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build without GUI dependencies (recommended for servers)
|
|
||||||
cargo build --release --bin api_server --no-default-features
|
|
||||||
|
|
||||||
# Or build with GUI support (if system libraries are available)
|
|
||||||
cargo build --release --bin api_server --features gui
|
|
||||||
```
|
|
||||||
|
|
||||||
The compiled binary will be at `target/release/api_server`.
|
|
||||||
|
|
||||||
### 2. Web Frontend
|
|
||||||
|
|
||||||
Build the optimized web frontend:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd web
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
The built files will be in `web/dist/`.
|
|
||||||
|
|
||||||
### 3. Desktop GUI (Optional)
|
|
||||||
|
|
||||||
Build the desktop application:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install Tauri CLI if not already installed
|
|
||||||
cargo install tauri-cli
|
|
||||||
|
|
||||||
# Build the desktop app
|
|
||||||
cargo tauri build --features gui
|
|
||||||
```
|
|
||||||
|
|
||||||
The built application will be in `src-tauri/target/release/bundle/`.
|
|
||||||
|
|
||||||
## Deployment Options
|
|
||||||
|
|
||||||
### Option 1: Single Server Deployment
|
|
||||||
|
|
||||||
Deploy both API and web frontend on the same server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Copy the API server binary
|
|
||||||
cp target/release/api_server /opt/orbital_simulator/
|
|
||||||
|
|
||||||
# 2. Copy the web files
|
|
||||||
cp -r web/dist /opt/orbital_simulator/static
|
|
||||||
|
|
||||||
# 3. Create systemd service
|
|
||||||
sudo tee /etc/systemd/system/orbital-simulator.service > /dev/null <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Orbital Simulator API Server
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=orbital
|
|
||||||
WorkingDirectory=/opt/orbital_simulator
|
|
||||||
ExecStart=/opt/orbital_simulator/api_server
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
Environment=STATIC_DIR=/opt/orbital_simulator/static
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 4. Start the service
|
|
||||||
sudo systemctl enable orbital-simulator
|
|
||||||
sudo systemctl start orbital-simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Separate API and Web Servers
|
|
||||||
|
|
||||||
#### API Server (Backend)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Deploy API server only
|
|
||||||
cp target/release/api_server /opt/orbital_simulator/
|
|
||||||
```
|
|
||||||
|
|
||||||
Configure reverse proxy (nginx example):
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name api.yourdomain.com;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:4395;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Web Frontend (Static Hosting)
|
|
||||||
|
|
||||||
Deploy `web/dist/` to any static hosting service:
|
|
||||||
|
|
||||||
- **Nginx**: Copy files to web root
|
|
||||||
- **Apache**: Copy files to document root
|
|
||||||
- **CDN**: Upload to AWS S3, Cloudflare, etc.
|
|
||||||
|
|
||||||
Update the API endpoint in the frontend if needed.
|
|
||||||
|
|
||||||
### Option 3: Docker Deployment
|
|
||||||
|
|
||||||
Create `Dockerfile`:
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# Multi-stage build
|
|
||||||
FROM rust:1.70 as rust-builder
|
|
||||||
WORKDIR /app
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY src ./src
|
|
||||||
COPY build.rs ./
|
|
||||||
RUN cargo build --release --bin api_server --no-default-features
|
|
||||||
|
|
||||||
FROM node:18 as web-builder
|
|
||||||
WORKDIR /app
|
|
||||||
COPY web/package*.json ./
|
|
||||||
RUN npm install
|
|
||||||
COPY web ./
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
|
||||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=rust-builder /app/target/release/api_server ./
|
|
||||||
COPY --from=web-builder /app/dist ./static
|
|
||||||
EXPOSE 4395
|
|
||||||
CMD ["./api_server"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Build and run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t orbital-simulator .
|
|
||||||
docker run -p 4395:4395 orbital-simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
- `PORT`: Server port (default: 4395)
|
|
||||||
- `HOST`: Server host (default: 0.0.0.0)
|
|
||||||
- `STATIC_DIR`: Directory for static files (default: web/dist)
|
|
||||||
- `LOG_LEVEL`: Logging level (debug, info, warn, error)
|
|
||||||
- `MAX_SIMULATIONS`: Maximum concurrent simulations (default: 10)
|
|
||||||
|
|
||||||
### Example Production Configuration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export PORT=8080
|
|
||||||
export HOST=0.0.0.0
|
|
||||||
export LOG_LEVEL=info
|
|
||||||
export MAX_SIMULATIONS=50
|
|
||||||
export STATIC_DIR=/var/www/orbital-simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Tuning
|
|
||||||
|
|
||||||
### Rust API Server
|
|
||||||
|
|
||||||
1. **Enable release optimizations** in `Cargo.toml`:
|
|
||||||
```toml
|
|
||||||
[profile.release]
|
|
||||||
opt-level = 3
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
panic = 'abort'
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Limit concurrent simulations** to prevent resource exhaustion
|
|
||||||
|
|
||||||
3. **Use connection pooling** for database connections (if added)
|
|
||||||
|
|
||||||
### Web Frontend
|
|
||||||
|
|
||||||
1. **Enable gzip compression** in your web server
|
|
||||||
2. **Set appropriate cache headers** for static assets
|
|
||||||
3. **Use a CDN** for global distribution
|
|
||||||
4. **Implement lazy loading** for large datasets
|
|
||||||
|
|
||||||
## Monitoring and Logging
|
|
||||||
|
|
||||||
### Metrics to Monitor
|
|
||||||
|
|
||||||
- API response times
|
|
||||||
- Active simulation count
|
|
||||||
- Memory usage
|
|
||||||
- CPU usage
|
|
||||||
- WebSocket connection count
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
|
|
||||||
The application uses the `log` crate. Configure logging level:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RUST_LOG=info ./api_server
|
|
||||||
```
|
|
||||||
|
|
||||||
Log levels:
|
|
||||||
- `debug`: Detailed debugging information
|
|
||||||
- `info`: General operational information
|
|
||||||
- `warn`: Warning messages
|
|
||||||
- `error`: Error messages only
|
|
||||||
|
|
||||||
### Health Checks
|
|
||||||
|
|
||||||
The API server provides health check endpoints:
|
|
||||||
|
|
||||||
- `GET /api/health`: Basic health check
|
|
||||||
- `GET /api/metrics`: System metrics (if enabled)
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
1. **Use HTTPS** in production
|
|
||||||
2. **Configure CORS** appropriately for your domain
|
|
||||||
3. **Implement rate limiting** to prevent abuse
|
|
||||||
4. **Sanitize user inputs** in configuration uploads
|
|
||||||
5. **Run with minimal privileges** (non-root user)
|
|
||||||
6. **Keep dependencies updated** regularly
|
|
||||||
|
|
||||||
## Backup and Recovery
|
|
||||||
|
|
||||||
### Data to Backup
|
|
||||||
|
|
||||||
- Configuration files
|
|
||||||
- Simulation results (if persisted)
|
|
||||||
- User data (if user accounts are added)
|
|
||||||
|
|
||||||
### Recovery Procedures
|
|
||||||
|
|
||||||
1. Restore application binary
|
|
||||||
2. Restore configuration files
|
|
||||||
3. Restart services
|
|
||||||
4. Verify functionality
|
|
||||||
|
|
||||||
## Scaling
|
|
||||||
|
|
||||||
### Horizontal Scaling
|
|
||||||
|
|
||||||
- Deploy multiple API server instances behind a load balancer
|
|
||||||
- Use Redis for session storage (if sessions are added)
|
|
||||||
- Implement proper service discovery
|
|
||||||
|
|
||||||
### Vertical Scaling
|
|
||||||
|
|
||||||
- Increase server resources (CPU, RAM)
|
|
||||||
- Optimize simulation algorithms
|
|
||||||
- Use faster storage (SSD)
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **Port already in use**: Change PORT environment variable
|
|
||||||
2. **Static files not found**: Check STATIC_DIR path
|
|
||||||
3. **High memory usage**: Limit concurrent simulations
|
|
||||||
4. **Slow performance**: Enable release optimizations
|
|
||||||
|
|
||||||
### Debug Mode
|
|
||||||
|
|
||||||
Run with debug logging:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RUST_LOG=debug ./api_server
|
|
||||||
```
|
|
||||||
|
|
||||||
### Performance Profiling
|
|
||||||
|
|
||||||
Use `perf` or `valgrind` to profile the application:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
perf record --call-graph=dwarf ./api_server
|
|
||||||
perf report
|
|
||||||
```
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues and questions:
|
|
||||||
|
|
||||||
1. Check the logs for error messages
|
|
||||||
2. Verify configuration settings
|
|
||||||
3. Test with minimal configuration
|
|
||||||
4. Consult the main README.md for additional information
|
|
296
DOCKER.md
296
DOCKER.md
@ -1,296 +0,0 @@
|
|||||||
# Docker Deployment Guide
|
|
||||||
|
|
||||||
This guide covers deploying the Orbital Simulator using Docker.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Development Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build and run with Docker Compose
|
|
||||||
docker-compose up --build
|
|
||||||
|
|
||||||
# Access the application
|
|
||||||
open http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production Deployment with Nginx
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run with production profile (includes Nginx reverse proxy)
|
|
||||||
docker-compose --profile production up --build -d
|
|
||||||
|
|
||||||
# Access the application
|
|
||||||
open http://localhost
|
|
||||||
```
|
|
||||||
|
|
||||||
## Manual Docker Build
|
|
||||||
|
|
||||||
### Build the Image
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the Docker image
|
|
||||||
docker build -t orbital-simulator .
|
|
||||||
|
|
||||||
# Run the container
|
|
||||||
docker run -p 3000:3000 orbital-simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build with Custom Tag
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build with version tag
|
|
||||||
docker build -t orbital-simulator:v1.0.0 .
|
|
||||||
|
|
||||||
# Push to registry (after login)
|
|
||||||
docker tag orbital-simulator:v1.0.0 your-registry/orbital-simulator:v1.0.0
|
|
||||||
docker push your-registry/orbital-simulator:v1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
The application supports the following environment variables:
|
|
||||||
|
|
||||||
- `RUST_LOG`: Log level (default: `info`)
|
|
||||||
- `BIND_ADDRESS`: Server bind address (default: `0.0.0.0:3000`)
|
|
||||||
|
|
||||||
### Example with Environment Variables
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -p 3000:3000 \
|
|
||||||
-e RUST_LOG=debug \
|
|
||||||
-e BIND_ADDRESS=0.0.0.0:3000 \
|
|
||||||
orbital-simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
## Volume Mounts
|
|
||||||
|
|
||||||
### Configuration Files
|
|
||||||
|
|
||||||
Mount custom configuration files:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -p 3000:3000 \
|
|
||||||
-v $(pwd)/custom-config:/app/config:ro \
|
|
||||||
orbital-simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
### Persistent Logs
|
|
||||||
|
|
||||||
Mount logs directory for persistence:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -p 3000:3000 \
|
|
||||||
-v orbital-logs:/app/logs \
|
|
||||||
orbital-simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
## Health Checks
|
|
||||||
|
|
||||||
The container includes a health check that verifies the API is responding:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check container health
|
|
||||||
docker ps
|
|
||||||
|
|
||||||
# View health check logs
|
|
||||||
docker inspect --format='{{json .State.Health}}' <container-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production Considerations
|
|
||||||
|
|
||||||
### SSL/TLS Setup
|
|
||||||
|
|
||||||
For production, update the `nginx.conf` file and mount SSL certificates:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml
|
|
||||||
services:
|
|
||||||
nginx:
|
|
||||||
volumes:
|
|
||||||
- ./ssl:/etc/nginx/ssl:ro
|
|
||||||
- ./nginx-prod.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Limits
|
|
||||||
|
|
||||||
Set appropriate resource limits:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml
|
|
||||||
services:
|
|
||||||
orbital-simulator:
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '2.0'
|
|
||||||
memory: 2G
|
|
||||||
reservations:
|
|
||||||
memory: 512M
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scaling
|
|
||||||
|
|
||||||
For high availability, you can run multiple instances:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml
|
|
||||||
services:
|
|
||||||
orbital-simulator:
|
|
||||||
scale: 3
|
|
||||||
deploy:
|
|
||||||
replicas: 3
|
|
||||||
```
|
|
||||||
|
|
||||||
### Monitoring
|
|
||||||
|
|
||||||
Add monitoring with Prometheus/Grafana:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml
|
|
||||||
services:
|
|
||||||
prometheus:
|
|
||||||
image: prom/prometheus
|
|
||||||
ports:
|
|
||||||
- "9090:9090"
|
|
||||||
volumes:
|
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
|
||||||
|
|
||||||
grafana:
|
|
||||||
image: grafana/grafana
|
|
||||||
ports:
|
|
||||||
- "3001:3000"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Container Won't Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check logs
|
|
||||||
docker logs <container-id>
|
|
||||||
|
|
||||||
# Run interactively for debugging
|
|
||||||
docker run -it --entrypoint /bin/bash orbital-simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permission Issues
|
|
||||||
|
|
||||||
The container runs as a non-root user. If you encounter permission issues:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check file permissions
|
|
||||||
ls -la config/
|
|
||||||
|
|
||||||
# Fix permissions if needed
|
|
||||||
chmod -R 644 config/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Memory Issues
|
|
||||||
|
|
||||||
For large simulations, increase container memory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run with more memory
|
|
||||||
docker run -p 3000:3000 --memory=4g orbital-simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building for Different Architectures
|
|
||||||
|
|
||||||
### Multi-architecture Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create and use buildx builder
|
|
||||||
docker buildx create --use
|
|
||||||
|
|
||||||
# Build for multiple architectures
|
|
||||||
docker buildx build --platform linux/amd64,linux/arm64 \
|
|
||||||
-t orbital-simulator:latest --push .
|
|
||||||
```
|
|
||||||
|
|
||||||
### ARM64 Support
|
|
||||||
|
|
||||||
The Docker image supports ARM64 (Apple Silicon, ARM servers):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build specifically for ARM64
|
|
||||||
docker buildx build --platform linux/arm64 \
|
|
||||||
-t orbital-simulator:arm64 .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Updates and Maintenance
|
|
||||||
|
|
||||||
### Updating the Application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull latest changes
|
|
||||||
git pull
|
|
||||||
|
|
||||||
# Rebuild and restart
|
|
||||||
docker-compose down
|
|
||||||
docker-compose up --build -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database/State Cleanup
|
|
||||||
|
|
||||||
Since the application stores simulation state in memory, restart to clear:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Restart containers
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# Or rebuild for full cleanup
|
|
||||||
docker-compose down -v
|
|
||||||
docker-compose up --build -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## CI/CD Integration
|
|
||||||
|
|
||||||
### GitHub Actions Example
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# .github/workflows/docker.yml
|
|
||||||
name: Docker Build and Push
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
tags: ['v*']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
orbital-simulator:latest
|
|
||||||
orbital-simulator:${{ github.sha }}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
1. **Run as non-root**: The container runs as user `appuser`
|
|
||||||
2. **Read-only config**: Mount config directory as read-only
|
|
||||||
3. **Network security**: Use Nginx for SSL termination and rate limiting
|
|
||||||
4. **Update base images**: Regularly update the base Debian image
|
|
||||||
5. **Scan for vulnerabilities**: Use `docker scan` or similar tools
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Scan for vulnerabilities
|
|
||||||
docker scan orbital-simulator
|
|
||||||
```
|
|
104
Dockerfile
104
Dockerfile
@ -1,104 +0,0 @@
|
|||||||
# Multi-stage Docker build for Orbital Simulator
|
|
||||||
# Stage 1: Build the Rust backend
|
|
||||||
FROM rust:1.75-slim as rust-builder
|
|
||||||
|
|
||||||
# Install system dependencies for building
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
pkg-config \
|
|
||||||
libssl-dev \
|
|
||||||
libfontconfig1-dev \
|
|
||||||
libfreetype6-dev \
|
|
||||||
libglib2.0-dev \
|
|
||||||
libgtk-3-dev \
|
|
||||||
libpango1.0-dev \
|
|
||||||
libatk1.0-dev \
|
|
||||||
libgdk-pixbuf-2.0-dev \
|
|
||||||
libcairo2-dev \
|
|
||||||
libasound2-dev \
|
|
||||||
libudev-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy Cargo files for dependency caching
|
|
||||||
COPY Cargo.toml ./
|
|
||||||
COPY build.rs ./
|
|
||||||
|
|
||||||
# Create dummy source files to build dependencies
|
|
||||||
RUN mkdir src && \
|
|
||||||
echo "fn main() {}" > src/main.rs && \
|
|
||||||
mkdir -p src/bin && \
|
|
||||||
echo "fn main() {}" > src/bin/api_server.rs && \
|
|
||||||
echo "fn main() {}" > src/bin/simulator.rs && \
|
|
||||||
echo "fn main() {}" > src/bin/orbiter.rs
|
|
||||||
|
|
||||||
# Build dependencies (this layer will be cached)
|
|
||||||
RUN cargo build --release --bin api_server
|
|
||||||
|
|
||||||
# Remove dummy files
|
|
||||||
RUN rm -rf src
|
|
||||||
|
|
||||||
# Copy actual source code
|
|
||||||
COPY src ./src
|
|
||||||
COPY config ./config
|
|
||||||
|
|
||||||
# Build the actual application
|
|
||||||
RUN cargo build --release --bin api_server
|
|
||||||
|
|
||||||
# Stage 2: Build the React frontend
|
|
||||||
FROM node:18-alpine as frontend-builder
|
|
||||||
|
|
||||||
WORKDIR /app/web
|
|
||||||
|
|
||||||
# Copy package files for dependency caching
|
|
||||||
COPY web/package.json web/package-lock.json ./
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Copy source files
|
|
||||||
COPY web/ ./
|
|
||||||
|
|
||||||
# Build the frontend
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Stage 3: Runtime image
|
|
||||||
FROM debian:bookworm-slim
|
|
||||||
|
|
||||||
# Install runtime dependencies
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
ca-certificates \
|
|
||||||
libssl3 \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Create app user
|
|
||||||
RUN useradd -r -s /bin/false appuser
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the built Rust binary
|
|
||||||
COPY --from=rust-builder /app/target/release/api_server ./api_server
|
|
||||||
|
|
||||||
# Copy configuration files
|
|
||||||
COPY --from=rust-builder /app/config ./config
|
|
||||||
|
|
||||||
# Copy the built frontend
|
|
||||||
COPY --from=frontend-builder /app/web/dist ./web/dist
|
|
||||||
|
|
||||||
# Create necessary directories
|
|
||||||
RUN mkdir -p logs && \
|
|
||||||
chown -R appuser:appuser /app
|
|
||||||
|
|
||||||
# Switch to non-root user
|
|
||||||
USER appuser
|
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 4395
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:4395/api/configs || exit 1
|
|
||||||
|
|
||||||
# Start the application
|
|
||||||
CMD ["./api_server"]
|
|
160
INTERFACES.md
160
INTERFACES.md
@ -1,160 +0,0 @@
|
|||||||
# Orbital Simulator Interfaces
|
|
||||||
|
|
||||||
This project now includes multiple ways to interact with the orbital simulator:
|
|
||||||
|
|
||||||
## 1. Web Interface
|
|
||||||
|
|
||||||
A browser-based interface with real-time 3D visualization.
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
- Real-time 3D orbital visualization using Three.js
|
|
||||||
- Interactive controls (start, pause, stop, step)
|
|
||||||
- Multiple simulation management
|
|
||||||
- Custom configuration editor
|
|
||||||
- Live statistics and energy monitoring
|
|
||||||
- Responsive design
|
|
||||||
|
|
||||||
### Usage:
|
|
||||||
```bash
|
|
||||||
# Start the API server
|
|
||||||
cargo run --release --bin api_server
|
|
||||||
|
|
||||||
# In another terminal, start the web frontend
|
|
||||||
cd web
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# Open http://localhost:5173 in your browser
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Desktop GUI
|
|
||||||
|
|
||||||
A native desktop application using Tauri (Rust + Web technologies).
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
- All web interface features in a native app
|
|
||||||
- File system integration
|
|
||||||
- Native notifications
|
|
||||||
- System tray integration
|
|
||||||
- Better performance than browser
|
|
||||||
|
|
||||||
### Usage:
|
|
||||||
```bash
|
|
||||||
# Install Tauri CLI
|
|
||||||
cargo install tauri-cli
|
|
||||||
|
|
||||||
# Start development mode
|
|
||||||
cargo tauri dev
|
|
||||||
|
|
||||||
# Build for production
|
|
||||||
cargo tauri build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Command Line Interface (Original)
|
|
||||||
|
|
||||||
The original command-line tools are still available:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run simulation
|
|
||||||
cargo run --release --bin simulator -- \
|
|
||||||
--config config/planets.toml \
|
|
||||||
--time 365d \
|
|
||||||
--step-size 3600 \
|
|
||||||
--output-file trajectory.bin
|
|
||||||
|
|
||||||
# Visualize results
|
|
||||||
python3 plot_trajectories.py trajectory.bin --animate
|
|
||||||
|
|
||||||
# 3D real-time visualizer
|
|
||||||
cargo run --release --bin orbiter -- trajectory.bin
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
The web API server provides RESTful endpoints:
|
|
||||||
|
|
||||||
- `POST /api/simulations` - Create new simulation
|
|
||||||
- `GET /api/simulations` - List all simulations
|
|
||||||
- `GET /api/simulations/:id` - Get simulation state
|
|
||||||
- `POST /api/simulations/:id/control` - Control simulation (start/pause/stop)
|
|
||||||
- `DELETE /api/simulations/:id` - Delete simulation
|
|
||||||
- `GET /api/configs` - List available configurations
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Prerequisites:
|
|
||||||
- Rust (latest stable)
|
|
||||||
- Node.js 18+ (for web interface)
|
|
||||||
- Python 3.7+ (for visualization)
|
|
||||||
|
|
||||||
### Full Installation:
|
|
||||||
```bash
|
|
||||||
# Clone and build
|
|
||||||
git clone <repository-url>
|
|
||||||
cd orbital_simulator
|
|
||||||
|
|
||||||
# Install Rust dependencies
|
|
||||||
cargo build --release
|
|
||||||
|
|
||||||
# Install Node.js dependencies
|
|
||||||
cd web
|
|
||||||
npm install
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# Install Python dependencies
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Install Tauri CLI (optional, for desktop GUI)
|
|
||||||
cargo install tauri-cli
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quick Start:
|
|
||||||
```bash
|
|
||||||
# Start everything with one command
|
|
||||||
./start_interfaces.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This will launch:
|
|
||||||
- API server on port 3000
|
|
||||||
- Web interface on port 5173
|
|
||||||
- Desktop GUI (if Tauri is installed)
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Architecture:
|
|
||||||
```
|
|
||||||
┌─────────────────┐ ┌─────────────────┐
|
|
||||||
│ Desktop GUI │ │ Web Interface │
|
|
||||||
│ (Tauri) │ │ (React) │
|
|
||||||
└─────────┬───────┘ └─────────┬───────┘
|
|
||||||
│ │
|
|
||||||
└──────────┬───────────┘
|
|
||||||
│
|
|
||||||
┌─────────────────┐
|
|
||||||
│ API Server │
|
|
||||||
│ (Axum) │
|
|
||||||
└─────────┬───────┘
|
|
||||||
│
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Rust Simulator │
|
|
||||||
│ (Core Logic) │
|
|
||||||
└─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding Features:
|
|
||||||
1. **Backend**: Add endpoints in `src/bin/api_server.rs`
|
|
||||||
2. **Web**: Add components in `web/src/components/`
|
|
||||||
3. **Desktop**: Add commands in `src-tauri/src/main.rs`
|
|
||||||
|
|
||||||
### Testing:
|
|
||||||
```bash
|
|
||||||
# Test Rust code
|
|
||||||
cargo test
|
|
||||||
|
|
||||||
# Test web interface
|
|
||||||
cd web
|
|
||||||
npm test
|
|
||||||
|
|
||||||
# Test API endpoints
|
|
||||||
curl http://localhost:3000/api/simulations
|
|
||||||
```
|
|
88
PLOTTING.md
88
PLOTTING.md
@ -1,88 +0,0 @@
|
|||||||
# Trajectory Plotting
|
|
||||||
|
|
||||||
This Python script can read and visualize the binary trajectory files generated by the orbital simulator.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
First, install the required Python dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Basic Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Plot trajectories from a simulation output file
|
|
||||||
python plot_trajectories.py trajectory_output.bin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Options
|
|
||||||
|
|
||||||
- `--2d-only`: Only show 2D plot (X-Y plane)
|
|
||||||
- `--3d-only`: Only show 3D plot
|
|
||||||
- `--energy`: Also show energy conservation plot
|
|
||||||
- `--animate`: Show animated trajectories with real-time simulation
|
|
||||||
- `--save-animation <filename>`: Save animation as MP4 file (requires ffmpeg)
|
|
||||||
- `--static`: Show static plots (default behavior)
|
|
||||||
- `--interval <ms>`: Animation frame interval in milliseconds (default: auto-scaled)
|
|
||||||
- `--target-duration <seconds>`: Target animation duration (default: 60)
|
|
||||||
- `--center <body_name>`: Center animation/plot on specified body (e.g., "Sun", "Earth")
|
|
||||||
- `--list-bodies`: List available bodies in trajectory file and exit
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run a simulation and plot the results
|
|
||||||
cargo run --bin simulator -- --config config/planets.json --time 365d --step-size 3600 --output-file solar_system.bin
|
|
||||||
python plot_trajectories.py solar_system.bin
|
|
||||||
|
|
||||||
# List available bodies in trajectory file
|
|
||||||
python plot_trajectories.py solar_system.bin --list-bodies
|
|
||||||
|
|
||||||
# Show animated trajectories centered on the Sun
|
|
||||||
python plot_trajectories.py solar_system.bin --animate --center Sun
|
|
||||||
|
|
||||||
# Show Earth-centered view (great for seeing Moon's orbit)
|
|
||||||
python plot_trajectories.py solar_system.bin --animate --center Earth --2d-only
|
|
||||||
|
|
||||||
# Show only 2D animation with custom speed
|
|
||||||
python plot_trajectories.py solar_system.bin --animate --2d-only --interval 100
|
|
||||||
|
|
||||||
# Save animation as MP4 file (Sun-centered)
|
|
||||||
python plot_trajectories.py solar_system.bin --animate --center Sun --save-animation solar_system_animation
|
|
||||||
|
|
||||||
# Show 3D trajectories with energy plot (static, Earth-centered)
|
|
||||||
python plot_trajectories.py solar_system.bin --3d-only --energy --static --center Earth
|
|
||||||
```
|
|
||||||
|
|
||||||
## Output
|
|
||||||
|
|
||||||
The script can display:
|
|
||||||
|
|
||||||
### Static Plots
|
|
||||||
- **2D Plot**: Trajectories in the X-Y plane with starting positions (circles) and ending positions (squares)
|
|
||||||
- **3D Plot**: Full 3D orbital trajectories
|
|
||||||
- **Energy Plot** (optional): Shows kinetic and total energy over time to verify energy conservation
|
|
||||||
|
|
||||||
### Animated Plots
|
|
||||||
- **2D Animation**: Real-time orbital motion in the X-Y plane with time display
|
|
||||||
- **3D Animation**: Full 3D animated orbital trajectories
|
|
||||||
- **Time Display**: Shows current simulation time in seconds/hours/days
|
|
||||||
- **Trails**: Recent positions shown as fading trails behind each body
|
|
||||||
- **MP4 Export**: Save animations as video files (requires ffmpeg)
|
|
||||||
|
|
||||||
Each celestial body is plotted in a different color with its name in the legend.
|
|
||||||
|
|
||||||
## File Format
|
|
||||||
|
|
||||||
The binary trajectory file contains serialized snapshots with:
|
|
||||||
- Timestamp (f64)
|
|
||||||
- Array of bodies, each containing:
|
|
||||||
- Name (String)
|
|
||||||
- Mass (f64)
|
|
||||||
- Position (3x f64)
|
|
||||||
- Velocity (3x f64)
|
|
||||||
- Acceleration (3x f64)
|
|
243
README.md
243
README.md
@ -1,214 +1,83 @@
|
|||||||
# Orbital Simulator
|
# Orbital Simulator
|
||||||
|
|
||||||
A comprehensive N-body orbital mechanics simulator with multiple interfaces: **Web Browser**, **Desktop GUI**, **CLI**, and **Python Tools**. Built in Rust for performance with React/Three.js for visualization.
|
A C++ implementation of an N-body orbital simulator with high-precision calculations.
|
||||||
|
|
||||||
## 🚀 Features
|
## Dependencies
|
||||||
|
|
||||||
### Core Simulation
|
- C++17 compatible compiler
|
||||||
- High-performance N-body gravitational simulation
|
- CMake 3.10 or higher
|
||||||
- Normalized units for numerical stability
|
- Boost library (for high-precision decimal arithmetic)
|
||||||
- Real-time and batch processing modes
|
- Optional: ncurses (for terminal plotting)
|
||||||
- Energy conservation monitoring
|
|
||||||
- Configurable time steps and integration methods
|
|
||||||
|
|
||||||
### Multiple Interfaces
|
## Building
|
||||||
- **🌐 Web Interface**: Browser-based with 3D visualization
|
|
||||||
- **🖥️ Desktop GUI**: Native application with Tauri
|
|
||||||
- **⚡ CLI Tools**: Command-line batch processing
|
|
||||||
- **📊 Python Tools**: Scientific plotting and analysis
|
|
||||||
- **🔌 REST API**: Programmatic access and integration
|
|
||||||
|
|
||||||
### Visualization
|
1. Create a build directory:
|
||||||
- Real-time 3D orbital mechanics
|
|
||||||
- Interactive camera controls
|
|
||||||
- Particle trails and body labels
|
|
||||||
- Energy plots and statistics
|
|
||||||
- Animation export capabilities
|
|
||||||
|
|
||||||
## 📦 Installation
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Rust (2021 edition or later)
|
|
||||||
- Node.js 18+ and npm
|
|
||||||
- Python 3.7+ (for analysis tools)
|
|
||||||
- Git
|
|
||||||
|
|
||||||
### Quick Setup
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repository-url>
|
mkdir build
|
||||||
cd orbital_simulator
|
cd build
|
||||||
chmod +x start_interfaces.sh test_interfaces.sh
|
|
||||||
./test_interfaces.sh # Verify everything works
|
|
||||||
./start_interfaces.sh # Start all interfaces
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual Installation
|
2. Configure with CMake:
|
||||||
```bash
|
```bash
|
||||||
# Install Rust dependencies
|
cmake ..
|
||||||
cargo build --release --no-default-features
|
|
||||||
|
|
||||||
# Install web dependencies
|
|
||||||
cd web && npm install && cd ..
|
|
||||||
|
|
||||||
# Install Python dependencies
|
|
||||||
python3 -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Optional: Install Tauri for desktop GUI
|
|
||||||
cargo install tauri-cli
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎯 Quick Start
|
3. Build the project:
|
||||||
|
|
||||||
### Web Interface (Recommended)
|
|
||||||
```bash
|
```bash
|
||||||
./start_interfaces.sh
|
make
|
||||||
# Open http://localhost:5173 in your browser
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### CLI Simulation
|
To enable terminal plotting with ncurses, configure with:
|
||||||
```bash
|
```bash
|
||||||
cargo run --release --bin simulator -- \
|
cmake -DENABLE_NCURSES=ON ..
|
||||||
--config config/inner_solar_system.toml \
|
|
||||||
--time 365d \
|
|
||||||
--step-size 3600 \
|
|
||||||
--output-file solar_system.bin
|
|
||||||
|
|
||||||
python3 plot_trajectories.py solar_system.bin --animate
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Configuration files define the initial state of your celestial bodies:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[bodies]]
|
|
||||||
name = "Sun"
|
|
||||||
mass = 1.989e30
|
|
||||||
position = [0.0, 0.0, 0.0]
|
|
||||||
velocity = [0.0, 0.0, 0.0]
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Earth"
|
|
||||||
mass = 5.972e24
|
|
||||||
position = [1.496e11, 0.0, 0.0] # 1 AU from Sun
|
|
||||||
velocity = [0.0, 29789.0, 0.0] # Orbital velocity
|
|
||||||
|
|
||||||
# Optionally specify custom units
|
|
||||||
[normalization]
|
|
||||||
m_0 = 5.972e24 # Earth mass
|
|
||||||
r_0 = 6.378e6 # Earth radius
|
|
||||||
t_0 = 5023.0 # Time unit
|
|
||||||
```
|
|
||||||
|
|
||||||
Several configurations are included:
|
|
||||||
- `planets.toml` - Complete solar system (16 bodies)
|
|
||||||
- `solar_system.toml` - Major planets only (9 bodies)
|
|
||||||
- `inner_solar_system.toml` - Inner planets + Moon (6 bodies)
|
|
||||||
- `earthsun_corrected.toml` - Simple Earth-Sun system (2 bodies)
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Running Simulations
|
The simulator can be used to simulate orbital mechanics with high precision. The main components are:
|
||||||
|
|
||||||
```bash
|
- `Body`: Represents a celestial body with position, velocity, and mass
|
||||||
cargo run --bin simulator -- [OPTIONS]
|
- `Simulator`: Manages the simulation of multiple bodies
|
||||||
|
- `units.hpp`: Contains physical constants and unit conversion utilities
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "simulator.hpp"
|
||||||
|
#include "units.hpp"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// Create bodies
|
||||||
|
std::vector<Body> bodies;
|
||||||
|
|
||||||
|
// Earth
|
||||||
|
Position earth_pos{Decimal(0), Decimal(0), Decimal(0)};
|
||||||
|
Velocity earth_vel{Decimal(0), Decimal(0), Decimal(0)};
|
||||||
|
bodies.emplace_back(earth_pos, earth_vel, EARTH_MASS, "Earth");
|
||||||
|
|
||||||
|
// Moon
|
||||||
|
Position moon_pos{AU, Decimal(0), Decimal(0)};
|
||||||
|
Velocity moon_vel{Decimal(0), MOON_ORBITAL_VELOCITY, Decimal(0)};
|
||||||
|
bodies.emplace_back(moon_pos, moon_vel, MOON_MASS, "Moon");
|
||||||
|
|
||||||
|
// Create simulator
|
||||||
|
Simulator sim(bodies, 0.1, 100, "output.txt");
|
||||||
|
|
||||||
|
// Run simulation
|
||||||
|
sim.run(1000);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Key options:
|
## Features
|
||||||
- `-c, --config <FILE>` - Configuration file
|
|
||||||
- `-t, --time <DURATION>` - How long to simulate (e.g., 10s, 5m, 2h, 100d)
|
|
||||||
- `-s, --step-size <SECONDS>` - Integration step size (default: 10.0)
|
|
||||||
- `-o, --output-file <FILE>` - Where to save trajectory data
|
|
||||||
- `-w, --force-overwrite` - Skip confirmation when overwriting files
|
|
||||||
|
|
||||||
### Visualization
|
- High-precision decimal arithmetic using Boost.Multiprecision
|
||||||
|
- N-body gravitational simulation
|
||||||
```bash
|
- Progress tracking and checkpointing
|
||||||
python3 plot_trajectories.py [OPTIONS] <trajectory_file>
|
- Optional terminal visualization
|
||||||
```
|
- Configurable simulation parameters
|
||||||
|
|
||||||
Useful options:
|
|
||||||
- `--animate` - Show animated trajectories instead of static plots
|
|
||||||
- `--center <BODY>` - Center the view on a specific body
|
|
||||||
- `--save-animation <PREFIX>` - Export animation as MP4 video
|
|
||||||
- `--energy` - Include energy conservation plots
|
|
||||||
- `--list-bodies` - Show what bodies are in the trajectory file
|
|
||||||
- `--2d-only` or `--3d-only` - Limit to 2D or 3D plots
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# See what bodies are available
|
|
||||||
python3 plot_trajectories.py trajectory.bin --list-bodies
|
|
||||||
|
|
||||||
# Animate from different perspectives
|
|
||||||
python3 plot_trajectories.py trajectory.bin --animate --center Sun
|
|
||||||
python3 plot_trajectories.py trajectory.bin --animate --center Jupiter
|
|
||||||
|
|
||||||
# Create a video
|
|
||||||
python3 plot_trajectories.py trajectory.bin --animate --save-animation solar_system
|
|
||||||
|
|
||||||
# Check energy conservation
|
|
||||||
python3 plot_trajectories.py trajectory.bin --energy
|
|
||||||
```
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
The simulator uses Newtonian gravity (F = G·m₁·m₂/r²) with explicit Euler integration. All bodies interact gravitationally with each other. The system normalizes units to Earth-based scales by default but you can specify custom normalization constants.
|
|
||||||
|
|
||||||
Animations automatically scale to about 60 seconds and show the time compression ratio (like "3.6 hours of simulation per second"). You can center the view on any body to see orbital mechanics from different reference frames.
|
|
||||||
|
|
||||||
The simulator includes safety features like confirmation prompts before overwriting files, and exports data in an efficient binary format.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── bin/
|
|
||||||
│ ├── simulator.rs # Main simulation program
|
|
||||||
│ └── orbiter.rs # 3D visualizer
|
|
||||||
├── config.rs # Configuration loading
|
|
||||||
├── simulation.rs # Physics simulation
|
|
||||||
├── types.rs # Data types and units
|
|
||||||
└── lib.rs # Library interface
|
|
||||||
|
|
||||||
config/ # Pre-made configurations
|
|
||||||
plot_trajectories.py # Visualization script
|
|
||||||
inspect_trajectories.py # Data inspection tool
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License - see source for details.
|
This project is open source and available under the MIT License.
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
Thomas Faour
|
|
||||||
|
|
||||||
## 🐳 Docker Deployment
|
|
||||||
|
|
||||||
The easiest way to deploy the Orbital Simulator is using Docker:
|
|
||||||
|
|
||||||
### Quick Start with Docker
|
|
||||||
```bash
|
|
||||||
# Clone the repository
|
|
||||||
git clone <repository-url>
|
|
||||||
cd orbital_simulator
|
|
||||||
|
|
||||||
# Start with Docker Compose
|
|
||||||
docker-compose up --build
|
|
||||||
|
|
||||||
# Access the application at http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production Deployment
|
|
||||||
```bash
|
|
||||||
# Start with Nginx reverse proxy
|
|
||||||
docker-compose --profile production up --build -d
|
|
||||||
|
|
||||||
# Access at http://localhost (port 80)
|
|
||||||
```
|
|
||||||
|
|
||||||
See [DOCKER.md](DOCKER.md) for detailed deployment documentation.
|
|
223
STATUS.md
223
STATUS.md
@ -1,223 +0,0 @@
|
|||||||
# 🚀 Orbital Simulator - Implementation Complete!
|
|
||||||
|
|
||||||
## ✅ Project Status: **COMPLETE**
|
|
||||||
|
|
||||||
The Orbital Simulator has been successfully expanded from a CLI-only tool to a comprehensive multi-interface application with real-time visualization capabilities.
|
|
||||||
|
|
||||||
## 🎯 Completed Features
|
|
||||||
|
|
||||||
### ✅ Core Simulation Engine
|
|
||||||
- High-performance N-body gravitational simulation in Rust
|
|
||||||
- Normalized units for numerical stability
|
|
||||||
- Real-time physics loop with configurable time steps
|
|
||||||
- Energy conservation monitoring
|
|
||||||
- Thread-safe simulation session management
|
|
||||||
|
|
||||||
### ✅ Web Interface (React + Three.js)
|
|
||||||
- **Frontend**: Modern React application with TypeScript
|
|
||||||
- **3D Visualization**: Real-time orbital mechanics using Three.js
|
|
||||||
- **Interactive Controls**: Play/pause, step, stop simulations
|
|
||||||
- **Configuration Panel**: Create simulations with preset or custom configs
|
|
||||||
- **Real-time Updates**: WebSocket-like polling for live data
|
|
||||||
- **Visual Features**: Particle trails, body labels, orbital paths
|
|
||||||
|
|
||||||
### ✅ REST API Server (Axum)
|
|
||||||
- **Full CRUD**: Create, read, update, delete simulations
|
|
||||||
- **Real-time Control**: Start, pause, stop, step operations
|
|
||||||
- **Session Management**: Multiple concurrent simulations
|
|
||||||
- **Static Serving**: Integrated web frontend serving
|
|
||||||
- **CORS Support**: Cross-origin resource sharing enabled
|
|
||||||
|
|
||||||
### ✅ Desktop GUI Ready (Tauri)
|
|
||||||
- **Native App**: Tauri-based desktop application framework
|
|
||||||
- **System Integration**: Native OS integration capabilities
|
|
||||||
- **Optional Install**: Feature-gated for flexible deployment
|
|
||||||
|
|
||||||
### ✅ CLI Tools (Maintained)
|
|
||||||
- **Batch Processing**: Original high-performance simulator
|
|
||||||
- **Configuration**: TOML/JSON config file support
|
|
||||||
- **Output Formats**: Binary trajectory files for analysis
|
|
||||||
|
|
||||||
### ✅ Python Analysis Tools (Enhanced)
|
|
||||||
- **Plotting**: 2D/3D trajectory visualization
|
|
||||||
- **Animation**: MP4 video export capabilities
|
|
||||||
- **Analysis**: Energy conservation and orbital mechanics analysis
|
|
||||||
|
|
||||||
## 🏗️ Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
||||||
│ Web Browser │ │ Desktop GUI │ │ CLI Tools │
|
|
||||||
│ (React/Three) │ │ (Tauri) │ │ (Rust Bins) │
|
|
||||||
└─────────┬───────┘ └────────┬─────────┘ └─────────────────┘
|
|
||||||
│ │
|
|
||||||
│ HTTP/JSON │ Direct API
|
|
||||||
│ │
|
|
||||||
┌────▼─────────────────────▼────┐
|
|
||||||
│ Rust API Server │
|
|
||||||
│ (Axum) │
|
|
||||||
│ ┌─────────────────────────┐ │
|
|
||||||
│ │ Simulation Engine │ │
|
|
||||||
│ │ (N-body Physics) │ │
|
|
||||||
│ └─────────────────────────┘ │
|
|
||||||
└─────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌───────▼────────┐
|
|
||||||
│ Python Tools │
|
|
||||||
│ (matplotlib) │
|
|
||||||
└────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚦 Getting Started
|
|
||||||
|
|
||||||
### Quick Start (All Interfaces)
|
|
||||||
```bash
|
|
||||||
git clone <repository>
|
|
||||||
cd orbital_simulator
|
|
||||||
./start_interfaces.sh
|
|
||||||
# Open http://localhost:5173 in browser
|
|
||||||
```
|
|
||||||
|
|
||||||
### Individual Components
|
|
||||||
```bash
|
|
||||||
# API Server only
|
|
||||||
cargo run --bin api_server --no-default-features
|
|
||||||
|
|
||||||
# Web development server
|
|
||||||
cd web && npm run dev
|
|
||||||
|
|
||||||
# CLI simulation
|
|
||||||
cargo run --bin simulator -- --config config/planets.toml --time 365d --step-size 3600 --output-file output.bin
|
|
||||||
|
|
||||||
# Python analysis
|
|
||||||
python3 plot_trajectories.py output.bin --animate
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
orbital_simulator/
|
|
||||||
├── src/
|
|
||||||
│ ├── bin/
|
|
||||||
│ │ ├── api_server.rs # REST API server
|
|
||||||
│ │ ├── simulator.rs # CLI batch simulator
|
|
||||||
│ │ └── orbiter.rs # 3D visualizer (Bevy - commented out)
|
|
||||||
│ ├── config.rs # Configuration structures
|
|
||||||
│ ├── simulation.rs # Core physics engine
|
|
||||||
│ ├── types.rs # Type definitions and utilities
|
|
||||||
│ └── lib.rs # Library exports
|
|
||||||
├── web/ # React web frontend
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── components/ # React components
|
|
||||||
│ │ ├── App.tsx # Main application
|
|
||||||
│ │ └── main.tsx # Entry point
|
|
||||||
│ ├── package.json # Node.js dependencies
|
|
||||||
│ └── vite.config.ts # Build configuration
|
|
||||||
├── src-tauri/ # Desktop GUI (Tauri)
|
|
||||||
│ ├── src/main.rs # Tauri entry point
|
|
||||||
│ ├── Cargo.toml # Tauri dependencies
|
|
||||||
│ └── tauri.conf.json # Tauri configuration
|
|
||||||
├── config/ # Simulation configurations
|
|
||||||
│ └── planets.toml # Sample planetary system
|
|
||||||
├── *.py # Python analysis tools
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── INTERFACES.md # Interface documentation
|
|
||||||
├── DEPLOYMENT.md # Production deployment guide
|
|
||||||
├── start_interfaces.sh # Multi-interface launcher
|
|
||||||
└── test_interfaces.sh # Comprehensive test suite
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🧪 Quality Assurance
|
|
||||||
|
|
||||||
### ✅ Automated Testing
|
|
||||||
- **16 Test Cases**: Comprehensive functionality verification
|
|
||||||
- **Build Tests**: All binaries compile successfully
|
|
||||||
- **Integration Tests**: API endpoints and data flow
|
|
||||||
- **Dependency Tests**: All package managers (cargo, npm, pip)
|
|
||||||
|
|
||||||
### ✅ Cross-Platform Support
|
|
||||||
- **Linux**: Fully tested and working
|
|
||||||
- **Windows**: Should work (Rust/Node.js/Python compatible)
|
|
||||||
- **macOS**: Should work (Rust/Node.js/Python compatible)
|
|
||||||
|
|
||||||
### ✅ Performance Optimized
|
|
||||||
- **Release Builds**: Optimized compilation for production
|
|
||||||
- **Memory Efficient**: Thread-safe simulation management
|
|
||||||
- **Scalable**: Multiple concurrent simulations supported
|
|
||||||
- **Real-time**: 30 FPS visualization updates
|
|
||||||
|
|
||||||
## 🚀 Deployment Options
|
|
||||||
|
|
||||||
### Development
|
|
||||||
```bash
|
|
||||||
./start_interfaces.sh # All interfaces with hot reload
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
```bash
|
|
||||||
# Build optimized binaries
|
|
||||||
cargo build --release --no-default-features
|
|
||||||
cd web && npm run build
|
|
||||||
|
|
||||||
# Deploy single-server with static files
|
|
||||||
./target/release/api_server
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
```bash
|
|
||||||
docker build -t orbital-simulator .
|
|
||||||
docker run -p 3000:3000 orbital-simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
See `DEPLOYMENT.md` for complete production setup instructions.
|
|
||||||
|
|
||||||
## 📊 Interface Comparison
|
|
||||||
|
|
||||||
| Feature | Web Interface | Desktop GUI | CLI Tools | Python Tools |
|
|
||||||
|---------|---------------|-------------|-----------|--------------|
|
|
||||||
| Real-time 3D | ✅ | ✅ | ❌ | ❌ |
|
|
||||||
| Interactive Controls | ✅ | ✅ | ❌ | ❌ |
|
|
||||||
| Batch Processing | ❌ | ❌ | ✅ | ✅ |
|
|
||||||
| Cross-Platform | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| Installation | Easy | Medium | Easy | Easy |
|
|
||||||
| Performance | High | Highest | Highest | Medium |
|
|
||||||
| Visualization | Excellent | Excellent | None | Excellent |
|
|
||||||
|
|
||||||
## 🎉 Success Metrics
|
|
||||||
|
|
||||||
- **✅ All original CLI functionality preserved**
|
|
||||||
- **✅ Real-time web visualization implemented**
|
|
||||||
- **✅ Multiple interface options available**
|
|
||||||
- **✅ Production-ready deployment guides**
|
|
||||||
- **✅ Comprehensive test coverage**
|
|
||||||
- **✅ Modern, maintainable codebase**
|
|
||||||
- **✅ Cross-platform compatibility**
|
|
||||||
- **✅ Performance optimized**
|
|
||||||
|
|
||||||
## 🔧 Maintenance
|
|
||||||
|
|
||||||
The project is designed for long-term maintainability:
|
|
||||||
|
|
||||||
- **Modular Architecture**: Clean separation of concerns
|
|
||||||
- **Type Safety**: Rust's type system prevents many bugs
|
|
||||||
- **Automated Testing**: Catches regressions early
|
|
||||||
- **Documentation**: Comprehensive guides and code comments
|
|
||||||
- **Standard Tools**: Uses well-supported frameworks and libraries
|
|
||||||
|
|
||||||
## 🚀 Next Steps (Optional Enhancements)
|
|
||||||
|
|
||||||
The core requirements are complete, but potential future enhancements include:
|
|
||||||
|
|
||||||
1. **User Authentication**: Multi-user support with saved simulations
|
|
||||||
2. **Persistent Storage**: Database for simulation history
|
|
||||||
3. **Advanced Physics**: Relativistic effects, radiation pressure
|
|
||||||
4. **Cloud Deployment**: Kubernetes, AWS/GCP deployments
|
|
||||||
5. **WebRTC**: True real-time streaming instead of polling
|
|
||||||
6. **Mobile App**: React Native or Flutter mobile client
|
|
||||||
7. **VR/AR Support**: Immersive 3D visualization
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Status**: ✅ **COMPLETE AND READY FOR USE**
|
|
||||||
|
|
||||||
All requirements have been successfully implemented and tested. The orbital simulator now provides a comprehensive suite of interfaces for different use cases while maintaining the high-performance core simulation engine.
|
|
135
animate.py
Normal file
135
animate.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.animation import FuncAnimation
|
||||||
|
from collections import defaultdict
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
|
||||||
|
def parse_line(line):
|
||||||
|
"""Parse a line from the simulation output."""
|
||||||
|
# Skip comments and empty lines
|
||||||
|
if line.startswith('#') or not line.strip():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Parse the line format: body_name: X = x1m x2m x3m, V = v1m/s v2m/s v3m/s
|
||||||
|
try:
|
||||||
|
# Split on colon to separate name and data
|
||||||
|
name_part, data_part = line.strip().split(':')
|
||||||
|
name = name_part.strip()
|
||||||
|
|
||||||
|
# Split data part into position and velocity
|
||||||
|
pos_part, vel_part = data_part.split(',')
|
||||||
|
|
||||||
|
# Extract position values
|
||||||
|
pos_str = pos_part.split('=')[1].strip()
|
||||||
|
pos_values = [float(x.replace('m', '').strip()) for x in pos_str.split() if x.strip()]
|
||||||
|
|
||||||
|
if len(pos_values) != 3:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'position': tuple(pos_values)
|
||||||
|
}
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read_output_file(filename):
|
||||||
|
"""Read the simulation output file and organize data by body and time."""
|
||||||
|
positions = defaultdict(list)
|
||||||
|
times = [] # We'll use frame numbers as times since actual time isn't in output
|
||||||
|
|
||||||
|
with open(filename, 'r') as f:
|
||||||
|
frame = 0
|
||||||
|
for line in f:
|
||||||
|
data = parse_line(line)
|
||||||
|
if data is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = data['name']
|
||||||
|
pos = data['position']
|
||||||
|
positions[name].append((pos[0], pos[1]))
|
||||||
|
frame += 1
|
||||||
|
times.append(frame)
|
||||||
|
|
||||||
|
return positions, times
|
||||||
|
|
||||||
|
def create_animation(positions, times, output_file=None):
|
||||||
|
"""Create an animation of the bodies' orbits."""
|
||||||
|
# Check if we have any data
|
||||||
|
if not positions or not times:
|
||||||
|
print("Error: No valid data found in the input file")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set up the figure and axis
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 10))
|
||||||
|
|
||||||
|
# Set equal aspect ratio
|
||||||
|
ax.set_aspect('equal')
|
||||||
|
|
||||||
|
# Create scatter plots for each body
|
||||||
|
scatters = {}
|
||||||
|
for name in positions.keys():
|
||||||
|
scatters[name], = ax.plot([], [], 'o-', label=name, alpha=0.7)
|
||||||
|
|
||||||
|
# Set up the plot
|
||||||
|
ax.set_xlabel('X (m)')
|
||||||
|
ax.set_ylabel('Y (m)')
|
||||||
|
ax.set_title('Orbital Simulation')
|
||||||
|
ax.legend()
|
||||||
|
|
||||||
|
# Find the bounds of the plot
|
||||||
|
all_x = []
|
||||||
|
all_y = []
|
||||||
|
for pos_list in positions.values():
|
||||||
|
if pos_list: # Only process if we have positions
|
||||||
|
x, y = zip(*pos_list)
|
||||||
|
all_x.extend(x)
|
||||||
|
all_y.extend(y)
|
||||||
|
|
||||||
|
if not all_x or not all_y:
|
||||||
|
print("Error: No valid position data found")
|
||||||
|
return
|
||||||
|
|
||||||
|
max_range = max(max(abs(min(all_x)), abs(max(all_x))),
|
||||||
|
max(abs(min(all_y)), abs(max(all_y))))
|
||||||
|
ax.set_xlim(-max_range, max_range)
|
||||||
|
ax.set_ylim(-max_range, max_range)
|
||||||
|
|
||||||
|
def init():
|
||||||
|
"""Initialize the animation."""
|
||||||
|
for scatter in scatters.values():
|
||||||
|
scatter.set_data([], [])
|
||||||
|
return list(scatters.values())
|
||||||
|
|
||||||
|
def update(frame):
|
||||||
|
"""Update the animation for each frame."""
|
||||||
|
for name, scatter in scatters.items():
|
||||||
|
if positions[name]: # Only update if we have positions
|
||||||
|
x, y = zip(*positions[name][:frame+1])
|
||||||
|
scatter.set_data(x, y)
|
||||||
|
return list(scatters.values())
|
||||||
|
|
||||||
|
# Create the animation
|
||||||
|
anim = FuncAnimation(fig, update, frames=len(times),
|
||||||
|
init_func=init, blit=True,
|
||||||
|
interval=50) # 50ms between frames
|
||||||
|
|
||||||
|
if output_file:
|
||||||
|
anim.save(output_file, writer='ffmpeg', fps=30)
|
||||||
|
else:
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='Animate orbital simulation output')
|
||||||
|
parser.add_argument('input_file', help='Input file from simulation')
|
||||||
|
parser.add_argument('--output', '-o', help='Output video file (optional)')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
positions, times = read_output_file(args.input_file)
|
||||||
|
create_animation(positions, times, args.output)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -1,11 +0,0 @@
|
|||||||
[[bodies]]
|
|
||||||
name = "Sun"
|
|
||||||
mass = 1.989e30
|
|
||||||
position = [0.0, 0.0, 0.0]
|
|
||||||
velocity = [0.0, 0.0, 0.0]
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Earth"
|
|
||||||
mass = 5.972e24
|
|
||||||
position = [1.496e11, 0.0, 0.0] # 1 AU
|
|
||||||
velocity = [0.0, 29789.0, 0.0] # Precise orbital velocity for circular orbit
|
|
@ -1,35 +0,0 @@
|
|||||||
[[bodies]]
|
|
||||||
name = "Sun"
|
|
||||||
mass = 1.989e30
|
|
||||||
position = [0.0, 0.0, 0.0]
|
|
||||||
velocity = [0.0, 0.0, 0.0]
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Mercury"
|
|
||||||
mass = 3.30104e23
|
|
||||||
position = [4.6000e10, 0.0, 0.0] # 0.307 AU
|
|
||||||
velocity = [0.0, 58970.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Venus"
|
|
||||||
mass = 4.8675e24
|
|
||||||
position = [1.08939e11, 0.0, 0.0] # 0.728 AU
|
|
||||||
velocity = [0.0, 34780.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Earth"
|
|
||||||
mass = 5.972e24
|
|
||||||
position = [1.496e11, 0.0, 0.0] # 1.000 AU
|
|
||||||
velocity = [0.0, 29789.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Moon"
|
|
||||||
mass = 7.34767309e22
|
|
||||||
position = [1.49984e11, 0.0, 0.0] # Earth + 384,400 km
|
|
||||||
velocity = [0.0, 30813.0, 0.0] # Earth velocity + moon orbital velocity
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Mars"
|
|
||||||
mass = 6.4171e23
|
|
||||||
position = [2.279e11, 0.0, 0.0] # 1.524 AU
|
|
||||||
velocity = [0.0, 24007.0, 0.0] # m/s
|
|
22
config/planets.json
Normal file
22
config/planets.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"planets": [
|
||||||
|
{
|
||||||
|
"name": "Earth",
|
||||||
|
"mass": 5.972e24,
|
||||||
|
"position": [149597870700, 0, 0],
|
||||||
|
"velocity": [0, 29780, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Moon",
|
||||||
|
"mass": 7.34767309e22,
|
||||||
|
"position": [149982270700, 0, 0],
|
||||||
|
"velocity": [0, 30802, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sun",
|
||||||
|
"mass": 1.989e30,
|
||||||
|
"position": [0, 0, 0],
|
||||||
|
"velocity": [0, 0, 0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,101 +0,0 @@
|
|||||||
# Simulation body configuration
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Sun"
|
|
||||||
mass = 1.989e30
|
|
||||||
position = [0.0, 0.0, 0.0]
|
|
||||||
velocity = [0.0, 0.0, 0.0]
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Mercury"
|
|
||||||
mass = 3.30104e23
|
|
||||||
position = [4.6000e10, 0.0, 0.0] # 0.307 AU
|
|
||||||
velocity = [0.0, 58970.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Venus"
|
|
||||||
mass = 4.867e24
|
|
||||||
position = [108941000000.0, 0.0, 0.0]
|
|
||||||
velocity = [0.0, 34780.0, 0.0]
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Earth"
|
|
||||||
mass = 5.972e24
|
|
||||||
position = [147095000000.0, 0.0, 0.0]
|
|
||||||
velocity = [0.0, 30290.0, 0.0]
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Moon"
|
|
||||||
mass = 7.34767309e22
|
|
||||||
position = [147458300000, 0.0, 0.0] # Earth + 384,400 km
|
|
||||||
velocity = [0.0, 31372.0, 0.0] # Earth velocity + moon orbital velocity
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Mars"
|
|
||||||
mass = 6.4171e23
|
|
||||||
position = [2.279e11, 0.0, 0.0] # 1.524 AU
|
|
||||||
velocity = [0.0, 24007.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Jupiter"
|
|
||||||
mass = 1.8982e27
|
|
||||||
position = [7.786e11, 0.0, 0.0] # 5.204 AU
|
|
||||||
velocity = [0.0, 13060.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Saturn"
|
|
||||||
mass = 5.6834e26
|
|
||||||
position = [1.432e12, 0.0, 0.0] # 9.573 AU
|
|
||||||
velocity = [0.0, 9640.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Uranus"
|
|
||||||
mass = 8.6810e25
|
|
||||||
position = [2.867e12, 0.0, 0.0] # 19.165 AU
|
|
||||||
velocity = [0.0, 6810.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Neptune"
|
|
||||||
mass = 1.02413e26
|
|
||||||
position = [4.515e12, 0.0, 0.0] # 30.178 AU
|
|
||||||
velocity = [0.0, 5430.0, 0.0] # m/s
|
|
||||||
|
|
||||||
# Dwarf planets and interesting objects
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Pluto"
|
|
||||||
mass = 1.303e22
|
|
||||||
position = [5.906e12, 0.0, 0.0] # 39.482 AU (average)
|
|
||||||
velocity = [0.0, 4670.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Ceres"
|
|
||||||
mass = 9.393e20
|
|
||||||
position = [4.14e11, 0.0, 0.0] # 2.766 AU (asteroid belt)
|
|
||||||
velocity = [0.0, 17880.0, 0.0] # m/s
|
|
||||||
|
|
||||||
# Some major moons for more interesting dynamics
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Io"
|
|
||||||
mass = 8.932e22
|
|
||||||
position = [7.790e11, 0.0, 0.0] # Jupiter + 421,700 km
|
|
||||||
velocity = [0.0, 30350.0, 0.0] # Jupiter velocity + Io orbital velocity
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Europa"
|
|
||||||
mass = 4.800e22
|
|
||||||
position = [7.793e11, 0.0, 0.0] # Jupiter + 671,034 km
|
|
||||||
velocity = [0.0, 26890.0, 0.0] # Jupiter velocity + Europa orbital velocity
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Ganymede"
|
|
||||||
mass = 1.482e23
|
|
||||||
position = [7.796e11, 0.0, 0.0] # Jupiter + 1,070,412 km
|
|
||||||
velocity = [0.0, 23250.0, 0.0] # Jupiter velocity + Ganymede orbital velocity
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Titan"
|
|
||||||
mass = 1.345e23
|
|
||||||
position = [1.433e12, 0.0, 0.0] # Saturn + 1,221,830 km
|
|
||||||
velocity = [0.0, 15100.0, 0.0] # Saturn velocity + Titan orbital velocity
|
|
16
config/planets_fake.json
Normal file
16
config/planets_fake.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"planets": [
|
||||||
|
{
|
||||||
|
"name": "Earth",
|
||||||
|
"mass": 7.342e24,
|
||||||
|
"position": [0, 0, 0],
|
||||||
|
"velocity": [0, -1022, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Moon",
|
||||||
|
"mass": 7.34767309e24,
|
||||||
|
"position": [384400000, 0, 0],
|
||||||
|
"velocity": [0, 1022, 0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,53 +0,0 @@
|
|||||||
[[bodies]]
|
|
||||||
name = "Sun"
|
|
||||||
mass = 1.989e30
|
|
||||||
position = [0.0, 0.0, 0.0]
|
|
||||||
velocity = [0.0, 0.0, 0.0]
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Mercury"
|
|
||||||
mass = 3.30104e23
|
|
||||||
position = [4.6000e10, 0.0, 0.0] # 0.307 AU
|
|
||||||
velocity = [0.0, 58970.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Venus"
|
|
||||||
mass = 4.8675e24
|
|
||||||
position = [1.08939e11, 0.0, 0.0] # 0.728 AU
|
|
||||||
velocity = [0.0, 34780.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Earth"
|
|
||||||
mass = 5.972e24
|
|
||||||
position = [1.496e11, 0.0, 0.0] # 1.000 AU
|
|
||||||
velocity = [0.0, 29789.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Mars"
|
|
||||||
mass = 6.4171e23
|
|
||||||
position = [2.279e11, 0.0, 0.0] # 1.524 AU
|
|
||||||
velocity = [0.0, 24007.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Jupiter"
|
|
||||||
mass = 1.8982e27
|
|
||||||
position = [7.786e11, 0.0, 0.0] # 5.204 AU
|
|
||||||
velocity = [0.0, 13060.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Saturn"
|
|
||||||
mass = 5.6834e26
|
|
||||||
position = [1.432e12, 0.0, 0.0] # 9.573 AU
|
|
||||||
velocity = [0.0, 9640.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Uranus"
|
|
||||||
mass = 8.6810e25
|
|
||||||
position = [2.867e12, 0.0, 0.0] # 19.165 AU
|
|
||||||
velocity = [0.0, 6810.0, 0.0] # m/s
|
|
||||||
|
|
||||||
[[bodies]]
|
|
||||||
name = "Neptune"
|
|
||||||
mass = 1.02413e26
|
|
||||||
position = [4.515e12, 0.0, 0.0] # 30.178 AU
|
|
||||||
velocity = [0.0, 5430.0, 0.0] # m/s
|
|
156
deploy.sh
156
deploy.sh
@ -1,156 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Orbital Simulator Docker Deployment Script
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🚀 Orbital Simulator Docker Deployment"
|
|
||||||
echo "======================================"
|
|
||||||
|
|
||||||
# Check if Docker is installed
|
|
||||||
if ! command -v docker &> /dev/null; then
|
|
||||||
echo "❌ Docker is not installed. Please install Docker first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if Docker Compose is available
|
|
||||||
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
|
||||||
echo "❌ Docker Compose is not installed. Please install Docker Compose first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Function to use docker-compose or docker compose
|
|
||||||
docker_compose() {
|
|
||||||
if command -v docker-compose &> /dev/null; then
|
|
||||||
docker-compose "$@"
|
|
||||||
else
|
|
||||||
docker compose "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse command line arguments
|
|
||||||
MODE="development"
|
|
||||||
ACTION="up"
|
|
||||||
BUILD="--build"
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--production)
|
|
||||||
MODE="production"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--dev|--development)
|
|
||||||
MODE="development"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--down)
|
|
||||||
ACTION="down"
|
|
||||||
BUILD=""
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--logs)
|
|
||||||
ACTION="logs"
|
|
||||||
BUILD=""
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--no-build)
|
|
||||||
BUILD=""
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--help)
|
|
||||||
echo "Usage: $0 [OPTIONS]"
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " --production Deploy in production mode with Nginx"
|
|
||||||
echo " --dev Deploy in development mode (default)"
|
|
||||||
echo " --down Stop and remove containers"
|
|
||||||
echo " --logs Show container logs"
|
|
||||||
echo " --no-build Skip building images"
|
|
||||||
echo " --help Show this help message"
|
|
||||||
echo ""
|
|
||||||
echo "Examples:"
|
|
||||||
echo " $0 # Start in development mode"
|
|
||||||
echo " $0 --production # Start in production mode"
|
|
||||||
echo " $0 --down # Stop all containers"
|
|
||||||
echo " $0 --logs # View logs"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
echo "Use --help for usage information"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Mode: $MODE"
|
|
||||||
echo "Action: $ACTION"
|
|
||||||
|
|
||||||
# Handle different actions
|
|
||||||
case $ACTION in
|
|
||||||
"up")
|
|
||||||
echo "🔧 Starting Orbital Simulator..."
|
|
||||||
|
|
||||||
if [ "$MODE" = "production" ]; then
|
|
||||||
echo "🌐 Production mode: Starting with Nginx reverse proxy"
|
|
||||||
docker_compose --profile production $ACTION $BUILD -d
|
|
||||||
echo ""
|
|
||||||
echo "✅ Orbital Simulator is running in production mode!"
|
|
||||||
echo "🌐 Web Interface: http://localhost"
|
|
||||||
echo "📊 Direct API: http://localhost:3000"
|
|
||||||
else
|
|
||||||
echo "🛠️ Development mode: Starting without reverse proxy"
|
|
||||||
docker_compose $ACTION $BUILD
|
|
||||||
echo ""
|
|
||||||
echo "✅ Orbital Simulator is running in development mode!"
|
|
||||||
echo "🌐 Web Interface: http://localhost:3000"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📋 Useful commands:"
|
|
||||||
echo " View logs: $0 --logs"
|
|
||||||
echo " Stop: $0 --down"
|
|
||||||
echo " Health check: docker ps"
|
|
||||||
;;
|
|
||||||
|
|
||||||
"down")
|
|
||||||
echo "🛑 Stopping Orbital Simulator..."
|
|
||||||
if [ "$MODE" = "production" ]; then
|
|
||||||
docker_compose --profile production down
|
|
||||||
else
|
|
||||||
docker_compose down
|
|
||||||
fi
|
|
||||||
echo "✅ All containers stopped and removed."
|
|
||||||
;;
|
|
||||||
|
|
||||||
"logs")
|
|
||||||
echo "📄 Showing container logs..."
|
|
||||||
if [ "$MODE" = "production" ]; then
|
|
||||||
docker_compose --profile production logs -f
|
|
||||||
else
|
|
||||||
docker_compose logs -f
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Health check function
|
|
||||||
check_health() {
|
|
||||||
echo "🏥 Checking health..."
|
|
||||||
|
|
||||||
# Wait a moment for containers to start
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
# Check if API is responding
|
|
||||||
if curl -f http://localhost:3000/api/configs &> /dev/null; then
|
|
||||||
echo "✅ API is healthy"
|
|
||||||
else
|
|
||||||
echo "⚠️ API health check failed"
|
|
||||||
echo "🔍 Try: docker ps"
|
|
||||||
echo "🔍 Try: $0 --logs"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Only run health check for 'up' action
|
|
||||||
if [ "$ACTION" = "up" ]; then
|
|
||||||
check_health
|
|
||||||
fi
|
|
@ -1,25 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
orbital-simulator:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
ports:
|
|
||||||
- "4395:4395"
|
|
||||||
environment:
|
|
||||||
- RUST_LOG=info
|
|
||||||
- BIND_ADDRESS=0.0.0.0:4395
|
|
||||||
volumes:
|
|
||||||
# Optional: Mount config directory for easy config updates
|
|
||||||
- ./config:/app/config:ro
|
|
||||||
# Optional: Mount logs directory
|
|
||||||
- ./orbital_logs:/app/logs
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:4395/api/configs || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
@ -1,347 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Inspect orbital trajectory data from binary files in a tabular format.
|
|
||||||
|
|
||||||
This script reads the binary trajectory files generated by the orbital simulator
|
|
||||||
and displays the data in a nicely formatted table for inspection and debugging.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python inspect_trajectories.py <trajectory_file.bin>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import struct
|
|
||||||
import argparse
|
|
||||||
import numpy as np
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
class BinaryReader:
|
|
||||||
def __init__(self, data):
|
|
||||||
self.data = data
|
|
||||||
self.pos = 0
|
|
||||||
|
|
||||||
def read_u64(self):
|
|
||||||
result = struct.unpack('<Q', self.data[self.pos:self.pos+8])[0]
|
|
||||||
self.pos += 8
|
|
||||||
return result
|
|
||||||
|
|
||||||
def read_f64(self):
|
|
||||||
result = struct.unpack('<d', self.data[self.pos:self.pos+8])[0]
|
|
||||||
self.pos += 8
|
|
||||||
return result
|
|
||||||
|
|
||||||
def read_string(self):
|
|
||||||
# Read length (u64) then string bytes
|
|
||||||
length = self.read_u64()
|
|
||||||
result = self.data[self.pos:self.pos+length].decode('utf-8')
|
|
||||||
self.pos += length
|
|
||||||
return result
|
|
||||||
|
|
||||||
def read_vec3(self):
|
|
||||||
# Read 3 f64 values for position/velocity/acceleration
|
|
||||||
x = self.read_f64()
|
|
||||||
y = self.read_f64()
|
|
||||||
z = self.read_f64()
|
|
||||||
return np.array([x, y, z])
|
|
||||||
|
|
||||||
def read_trajectory_file(filename):
|
|
||||||
"""Read the binary trajectory file and return parsed data."""
|
|
||||||
with open(filename, 'rb') as f:
|
|
||||||
data = f.read()
|
|
||||||
|
|
||||||
reader = BinaryReader(data)
|
|
||||||
snapshots = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
while reader.pos < len(data):
|
|
||||||
# Read snapshot
|
|
||||||
time = reader.read_f64()
|
|
||||||
|
|
||||||
# Read number of bodies (u64)
|
|
||||||
num_bodies = reader.read_u64()
|
|
||||||
|
|
||||||
bodies = []
|
|
||||||
for _ in range(num_bodies):
|
|
||||||
# Read Body struct
|
|
||||||
name = reader.read_string()
|
|
||||||
mass = reader.read_f64()
|
|
||||||
position = reader.read_vec3()
|
|
||||||
velocity = reader.read_vec3()
|
|
||||||
acceleration = reader.read_vec3()
|
|
||||||
|
|
||||||
bodies.append({
|
|
||||||
'name': name,
|
|
||||||
'mass': mass,
|
|
||||||
'position': position,
|
|
||||||
'velocity': velocity,
|
|
||||||
'acceleration': acceleration
|
|
||||||
})
|
|
||||||
|
|
||||||
snapshots.append({
|
|
||||||
'time': time,
|
|
||||||
'bodies': bodies
|
|
||||||
})
|
|
||||||
|
|
||||||
except struct.error:
|
|
||||||
# End of file or corrupted data
|
|
||||||
pass
|
|
||||||
|
|
||||||
return snapshots
|
|
||||||
|
|
||||||
def format_scientific(value, precision=3):
|
|
||||||
"""Format number in scientific notation with specified precision."""
|
|
||||||
if abs(value) < 1e-6 or abs(value) >= 1e6:
|
|
||||||
return f"{value:.{precision}e}"
|
|
||||||
else:
|
|
||||||
return f"{value:.{precision}f}"
|
|
||||||
|
|
||||||
def format_vector(vec, precision=3):
|
|
||||||
"""Format a 3D vector in a compact form."""
|
|
||||||
x, y, z = vec
|
|
||||||
return f"({format_scientific(x, precision)}, {format_scientific(y, precision)}, {format_scientific(z, precision)})"
|
|
||||||
|
|
||||||
def format_time(seconds):
|
|
||||||
"""Format time in a human-readable format."""
|
|
||||||
if seconds == 0:
|
|
||||||
return "0.0s"
|
|
||||||
elif seconds < 60:
|
|
||||||
return f"{seconds:.1f}s"
|
|
||||||
elif seconds < 3600:
|
|
||||||
return f"{seconds/60:.1f}m"
|
|
||||||
elif seconds < 86400:
|
|
||||||
return f"{seconds/3600:.1f}h"
|
|
||||||
else:
|
|
||||||
return f"{seconds/86400:.1f}d"
|
|
||||||
|
|
||||||
def print_summary_table(snapshots, max_rows=15):
|
|
||||||
"""Print a summary table of all snapshots."""
|
|
||||||
print("📊 TRAJECTORY SUMMARY")
|
|
||||||
print("=" * 100)
|
|
||||||
|
|
||||||
# Header
|
|
||||||
print(f"{'Step':<6} {'Time':<12} {'Bodies':<8} {'Sample Position (first body)':<35} {'Sample Velocity (first body)':<35}")
|
|
||||||
print("-" * 100)
|
|
||||||
|
|
||||||
# Determine which snapshots to show
|
|
||||||
total_snapshots = len(snapshots)
|
|
||||||
if total_snapshots <= max_rows:
|
|
||||||
indices = list(range(total_snapshots))
|
|
||||||
else:
|
|
||||||
# Show first few, middle few, and last few
|
|
||||||
start_count = max_rows // 3
|
|
||||||
end_count = max_rows // 3
|
|
||||||
middle_count = max_rows - start_count - end_count
|
|
||||||
|
|
||||||
indices = []
|
|
||||||
indices.extend(range(start_count))
|
|
||||||
|
|
||||||
if middle_count > 0:
|
|
||||||
middle_start = total_snapshots // 2 - middle_count // 2
|
|
||||||
indices.extend(range(middle_start, middle_start + middle_count))
|
|
||||||
|
|
||||||
indices.extend(range(total_snapshots - end_count, total_snapshots))
|
|
||||||
indices = sorted(set(indices)) # Remove duplicates and sort
|
|
||||||
|
|
||||||
prev_index = -1
|
|
||||||
for i, idx in enumerate(indices):
|
|
||||||
if idx > prev_index + 1 and prev_index >= 0:
|
|
||||||
print(" ...")
|
|
||||||
|
|
||||||
snapshot = snapshots[idx]
|
|
||||||
time_str = format_time(snapshot['time'])
|
|
||||||
body_count = len(snapshot['bodies'])
|
|
||||||
|
|
||||||
if snapshot['bodies']:
|
|
||||||
first_body = snapshot['bodies'][0]
|
|
||||||
pos_str = format_vector(first_body['position'], 2)
|
|
||||||
vel_str = format_vector(first_body['velocity'], 2)
|
|
||||||
else:
|
|
||||||
pos_str = "N/A"
|
|
||||||
vel_str = "N/A"
|
|
||||||
|
|
||||||
print(f"{idx:<6} {time_str:<12} {body_count:<8} {pos_str:<35} {vel_str:<35}")
|
|
||||||
prev_index = idx
|
|
||||||
|
|
||||||
print("-" * 100)
|
|
||||||
print(f"Total snapshots: {total_snapshots}")
|
|
||||||
|
|
||||||
def print_detailed_table(snapshots, body_name=None, max_rows=15):
|
|
||||||
"""Print detailed information for a specific body."""
|
|
||||||
if not snapshots:
|
|
||||||
print("No data to display!")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get all body names
|
|
||||||
all_bodies = set()
|
|
||||||
for snapshot in snapshots:
|
|
||||||
for body in snapshot['bodies']:
|
|
||||||
all_bodies.add(body['name'])
|
|
||||||
|
|
||||||
if body_name and body_name not in all_bodies:
|
|
||||||
print(f"❌ Body '{body_name}' not found. Available bodies: {', '.join(sorted(all_bodies))}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# If no body specified, show the first one
|
|
||||||
if not body_name:
|
|
||||||
body_name = sorted(all_bodies)[0]
|
|
||||||
|
|
||||||
print(f"🌍 DETAILED VIEW: {body_name}")
|
|
||||||
print("=" * 120)
|
|
||||||
|
|
||||||
# Header
|
|
||||||
print(f"{'Step':<6} {'Time':<12} {'Position (x, y, z)':<40} {'Velocity (x, y, z)':<40} {'|v|':<12} {'|a|':<12}")
|
|
||||||
print("-" * 120)
|
|
||||||
|
|
||||||
# Determine which snapshots to show
|
|
||||||
total_snapshots = len(snapshots)
|
|
||||||
if total_snapshots <= max_rows:
|
|
||||||
indices = list(range(total_snapshots))
|
|
||||||
else:
|
|
||||||
# Show first few, middle few, and last few
|
|
||||||
start_count = max_rows // 3
|
|
||||||
end_count = max_rows // 3
|
|
||||||
middle_count = max_rows - start_count - end_count
|
|
||||||
|
|
||||||
indices = []
|
|
||||||
indices.extend(range(start_count))
|
|
||||||
|
|
||||||
if middle_count > 0:
|
|
||||||
middle_start = total_snapshots // 2 - middle_count // 2
|
|
||||||
indices.extend(range(middle_start, middle_start + middle_count))
|
|
||||||
|
|
||||||
indices.extend(range(total_snapshots - end_count, total_snapshots))
|
|
||||||
indices = sorted(set(indices))
|
|
||||||
|
|
||||||
prev_index = -1
|
|
||||||
for idx in indices:
|
|
||||||
if idx > prev_index + 1 and prev_index >= 0:
|
|
||||||
print(" ...")
|
|
||||||
|
|
||||||
snapshot = snapshots[idx]
|
|
||||||
time_str = format_time(snapshot['time'])
|
|
||||||
|
|
||||||
# Find the body in this snapshot
|
|
||||||
body_data = None
|
|
||||||
for body in snapshot['bodies']:
|
|
||||||
if body['name'] == body_name:
|
|
||||||
body_data = body
|
|
||||||
break
|
|
||||||
|
|
||||||
if body_data:
|
|
||||||
pos_str = format_vector(body_data['position'], 3)
|
|
||||||
vel_str = format_vector(body_data['velocity'], 3)
|
|
||||||
vel_mag = np.linalg.norm(body_data['velocity'])
|
|
||||||
acc_mag = np.linalg.norm(body_data['acceleration'])
|
|
||||||
vel_mag_str = format_scientific(vel_mag, 2)
|
|
||||||
acc_mag_str = format_scientific(acc_mag, 2)
|
|
||||||
|
|
||||||
print(f"{idx:<6} {time_str:<12} {pos_str:<40} {vel_str:<40} {vel_mag_str:<12} {acc_mag_str:<12}")
|
|
||||||
else:
|
|
||||||
print(f"{idx:<6} {time_str:<12} {'BODY NOT FOUND':<40}")
|
|
||||||
|
|
||||||
prev_index = idx
|
|
||||||
|
|
||||||
print("-" * 120)
|
|
||||||
|
|
||||||
def print_statistics(snapshots):
|
|
||||||
"""Print statistical information about the trajectory."""
|
|
||||||
if not snapshots:
|
|
||||||
return
|
|
||||||
|
|
||||||
print("\n📈 TRAJECTORY STATISTICS")
|
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
# Time statistics
|
|
||||||
times = [s['time'] for s in snapshots]
|
|
||||||
time_start, time_end = times[0], times[-1]
|
|
||||||
duration = time_end - time_start
|
|
||||||
|
|
||||||
print(f"Time range: {format_time(time_start)} to {format_time(time_end)}")
|
|
||||||
print(f"Total duration: {format_time(duration)}")
|
|
||||||
print(f"Number of snapshots: {len(snapshots)}")
|
|
||||||
|
|
||||||
if len(times) > 1:
|
|
||||||
time_steps = [times[i+1] - times[i] for i in range(len(times)-1)]
|
|
||||||
avg_step = np.mean(time_steps)
|
|
||||||
print(f"Average time step: {format_time(avg_step)}")
|
|
||||||
|
|
||||||
# Body statistics
|
|
||||||
body_names = set()
|
|
||||||
for snapshot in snapshots:
|
|
||||||
for body in snapshot['bodies']:
|
|
||||||
body_names.add(body['name'])
|
|
||||||
|
|
||||||
print(f"Bodies tracked: {', '.join(sorted(body_names))}")
|
|
||||||
|
|
||||||
# Position and velocity ranges for each body
|
|
||||||
for body_name in sorted(body_names):
|
|
||||||
positions = []
|
|
||||||
velocities = []
|
|
||||||
|
|
||||||
for snapshot in snapshots:
|
|
||||||
for body in snapshot['bodies']:
|
|
||||||
if body['name'] == body_name:
|
|
||||||
positions.append(body['position'])
|
|
||||||
velocities.append(body['velocity'])
|
|
||||||
|
|
||||||
if positions:
|
|
||||||
positions = np.array(positions)
|
|
||||||
velocities = np.array(velocities)
|
|
||||||
|
|
||||||
pos_min = np.min(positions, axis=0)
|
|
||||||
pos_max = np.max(positions, axis=0)
|
|
||||||
vel_min = np.min(np.linalg.norm(velocities, axis=1))
|
|
||||||
vel_max = np.max(np.linalg.norm(velocities, axis=1))
|
|
||||||
|
|
||||||
print(f"\n{body_name}:")
|
|
||||||
print(f" Position range: X[{format_scientific(pos_min[0])}, {format_scientific(pos_max[0])}]")
|
|
||||||
print(f" Y[{format_scientific(pos_min[1])}, {format_scientific(pos_max[1])}]")
|
|
||||||
print(f" Z[{format_scientific(pos_min[2])}, {format_scientific(pos_max[2])}]")
|
|
||||||
print(f" Speed range: {format_scientific(vel_min)} to {format_scientific(vel_max)} m/s")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description='Inspect orbital trajectory data in tabular format')
|
|
||||||
parser.add_argument('trajectory_file', help='Binary trajectory file to inspect')
|
|
||||||
parser.add_argument('--rows', '-r', type=int, default=15, help='Maximum number of rows to display (default: 15)')
|
|
||||||
parser.add_argument('--body', '-b', type=str, help='Show detailed view for specific body')
|
|
||||||
parser.add_argument('--summary', '-s', action='store_true', help='Show summary of all snapshots')
|
|
||||||
parser.add_argument('--stats', action='store_true', help='Show trajectory statistics')
|
|
||||||
parser.add_argument('--all', '-a', action='store_true', help='Show all available information')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
print(f"🔍 Inspecting trajectory file: {args.trajectory_file}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
try:
|
|
||||||
snapshots = read_trajectory_file(args.trajectory_file)
|
|
||||||
|
|
||||||
if not snapshots:
|
|
||||||
print("❌ No data found in file!")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Determine what to show
|
|
||||||
show_summary = args.summary or args.all or (not args.body and not args.stats)
|
|
||||||
show_detailed = args.body or args.all
|
|
||||||
show_stats = args.stats or args.all
|
|
||||||
|
|
||||||
if show_summary:
|
|
||||||
print_summary_table(snapshots, args.rows)
|
|
||||||
print()
|
|
||||||
|
|
||||||
if show_detailed:
|
|
||||||
print_detailed_table(snapshots, args.body, args.rows)
|
|
||||||
print()
|
|
||||||
|
|
||||||
if show_stats:
|
|
||||||
print_statistics(snapshots)
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(f"❌ Error: File '{args.trajectory_file}' not found!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error reading file: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
72
nginx.conf
72
nginx.conf
@ -1,72 +0,0 @@
|
|||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
upstream orbital_backend {
|
|
||||||
server orbital-simulator:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Rate limiting
|
|
||||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
# Redirect HTTP to HTTPS in production
|
|
||||||
# return 301 https://$server_name$request_uri;
|
|
||||||
|
|
||||||
# For development, serve directly
|
|
||||||
location / {
|
|
||||||
proxy_pass http://orbital_backend;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# WebSocket support (if needed in future)
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
}
|
|
||||||
|
|
||||||
# API rate limiting
|
|
||||||
location /api/ {
|
|
||||||
limit_req zone=api burst=20 nodelay;
|
|
||||||
proxy_pass http://orbital_backend;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Static file caching
|
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
|
|
||||||
proxy_pass http://orbital_backend;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTPS server (uncomment for production with SSL certificates)
|
|
||||||
# server {
|
|
||||||
# listen 443 ssl http2;
|
|
||||||
# server_name your-domain.com;
|
|
||||||
#
|
|
||||||
# ssl_certificate /etc/nginx/ssl/cert.pem;
|
|
||||||
# ssl_certificate_key /etc/nginx/ssl/key.pem;
|
|
||||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
# ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
|
|
||||||
# ssl_prefer_server_ciphers off;
|
|
||||||
#
|
|
||||||
# location / {
|
|
||||||
# proxy_pass http://orbital_backend;
|
|
||||||
# proxy_set_header Host $host;
|
|
||||||
# proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
}
|
|
@ -1,570 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Plot orbital trajectories from binary output file generated by the orbital simulator.
|
|
||||||
|
|
||||||
The binary file contains snapshots serialized with bincode, where each snapshot has:
|
|
||||||
- time: f64
|
|
||||||
- bodies: array of Body structs
|
|
||||||
- name: String
|
|
||||||
- mass: f64
|
|
||||||
- position: [f64; 3] (x, y, z)
|
|
||||||
- velocity: [f64; 3] (vx, vy, vz)
|
|
||||||
- acceleration: [f64; 3] (ax, ay, az)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python plot_trajectories.py <trajectory_file.bin>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import struct
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
from mpl_toolkits.mplot3d import Axes3D
|
|
||||||
import matplotlib.animation as animation
|
|
||||||
import numpy as np
|
|
||||||
from collections import defaultdict
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
class BinaryReader:
|
|
||||||
def __init__(self, data):
|
|
||||||
self.data = data
|
|
||||||
self.pos = 0
|
|
||||||
|
|
||||||
def read_u64(self):
|
|
||||||
result = struct.unpack('<Q', self.data[self.pos:self.pos+8])[0]
|
|
||||||
self.pos += 8
|
|
||||||
return result
|
|
||||||
|
|
||||||
def read_f64(self):
|
|
||||||
result = struct.unpack('<d', self.data[self.pos:self.pos+8])[0]
|
|
||||||
self.pos += 8
|
|
||||||
return result
|
|
||||||
|
|
||||||
def read_string(self):
|
|
||||||
# Read length (u64) then string bytes
|
|
||||||
length = self.read_u64()
|
|
||||||
result = self.data[self.pos:self.pos+length].decode('utf-8')
|
|
||||||
self.pos += length
|
|
||||||
return result
|
|
||||||
|
|
||||||
def read_vec3(self):
|
|
||||||
# Read 3 f64 values for position/velocity/acceleration
|
|
||||||
x = self.read_f64()
|
|
||||||
y = self.read_f64()
|
|
||||||
z = self.read_f64()
|
|
||||||
return np.array([x, y, z])
|
|
||||||
|
|
||||||
def read_trajectory_file(filename):
|
|
||||||
"""Read the binary trajectory file and return parsed data."""
|
|
||||||
with open(filename, 'rb') as f:
|
|
||||||
data = f.read()
|
|
||||||
|
|
||||||
reader = BinaryReader(data)
|
|
||||||
snapshots = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
while reader.pos < len(data):
|
|
||||||
# Read snapshot
|
|
||||||
time = reader.read_f64()
|
|
||||||
|
|
||||||
# Read number of bodies (u64)
|
|
||||||
num_bodies = reader.read_u64()
|
|
||||||
|
|
||||||
bodies = []
|
|
||||||
for _ in range(num_bodies):
|
|
||||||
# Read Body struct
|
|
||||||
name = reader.read_string()
|
|
||||||
mass = reader.read_f64()
|
|
||||||
position = reader.read_vec3()
|
|
||||||
velocity = reader.read_vec3()
|
|
||||||
acceleration = reader.read_vec3()
|
|
||||||
|
|
||||||
bodies.append({
|
|
||||||
'name': name,
|
|
||||||
'mass': mass,
|
|
||||||
'position': position,
|
|
||||||
'velocity': velocity,
|
|
||||||
'acceleration': acceleration
|
|
||||||
})
|
|
||||||
|
|
||||||
snapshots.append({
|
|
||||||
'time': time,
|
|
||||||
'bodies': bodies
|
|
||||||
})
|
|
||||||
|
|
||||||
except struct.error:
|
|
||||||
# End of file or corrupted data
|
|
||||||
pass
|
|
||||||
|
|
||||||
return snapshots
|
|
||||||
|
|
||||||
def organize_trajectories(snapshots):
|
|
||||||
"""Organize snapshots into trajectories by body name."""
|
|
||||||
trajectories = defaultdict(list)
|
|
||||||
times = []
|
|
||||||
|
|
||||||
for snapshot in snapshots:
|
|
||||||
times.append(snapshot['time'])
|
|
||||||
for body in snapshot['bodies']:
|
|
||||||
trajectories[body['name']].append(body['position'])
|
|
||||||
|
|
||||||
# Convert lists to numpy arrays
|
|
||||||
for name in trajectories:
|
|
||||||
trajectories[name] = np.array(trajectories[name])
|
|
||||||
|
|
||||||
return dict(trajectories), np.array(times)
|
|
||||||
|
|
||||||
def plot_trajectories_2d(trajectories, times, center_body=None):
|
|
||||||
"""Plot 2D trajectories (X-Y plane)."""
|
|
||||||
plt.figure(figsize=(12, 10))
|
|
||||||
|
|
||||||
colors = plt.cm.tab10(np.linspace(0, 1, len(trajectories)))
|
|
||||||
|
|
||||||
for i, (name, positions) in enumerate(trajectories.items()):
|
|
||||||
x = positions[:, 0]
|
|
||||||
y = positions[:, 1]
|
|
||||||
|
|
||||||
plt.plot(x, y, color=colors[i], alpha=0.7, linewidth=1.5, label=name)
|
|
||||||
|
|
||||||
# Mark starting position
|
|
||||||
plt.plot(x[0], y[0], 'o', color=colors[i], markersize=8, alpha=0.8)
|
|
||||||
|
|
||||||
# Mark current position
|
|
||||||
plt.plot(x[-1], y[-1], 's', color=colors[i], markersize=6, alpha=0.8)
|
|
||||||
|
|
||||||
plt.xlabel('X Position (m)')
|
|
||||||
plt.ylabel('Y Position (m)')
|
|
||||||
|
|
||||||
title = 'Orbital Trajectories (X-Y Plane)'
|
|
||||||
if center_body:
|
|
||||||
title += f' - Centered on {center_body}'
|
|
||||||
plt.title(title)
|
|
||||||
|
|
||||||
plt.legend()
|
|
||||||
plt.grid(True, alpha=0.3)
|
|
||||||
plt.axis('equal')
|
|
||||||
plt.tight_layout()
|
|
||||||
|
|
||||||
def plot_trajectories_3d(trajectories, times, center_body=None):
|
|
||||||
"""Plot 3D trajectories."""
|
|
||||||
fig = plt.figure(figsize=(12, 10))
|
|
||||||
ax = fig.add_subplot(111, projection='3d')
|
|
||||||
|
|
||||||
colors = plt.cm.tab10(np.linspace(0, 1, len(trajectories)))
|
|
||||||
|
|
||||||
for i, (name, positions) in enumerate(trajectories.items()):
|
|
||||||
x = positions[:, 0]
|
|
||||||
y = positions[:, 1]
|
|
||||||
z = positions[:, 2]
|
|
||||||
|
|
||||||
ax.plot(x, y, z, color=colors[i], alpha=0.7, linewidth=1.5, label=name)
|
|
||||||
|
|
||||||
# Mark starting position
|
|
||||||
ax.scatter(x[0], y[0], z[0], color=colors[i], s=100, alpha=0.8, marker='o')
|
|
||||||
|
|
||||||
# Mark current position
|
|
||||||
ax.scatter(x[-1], y[-1], z[-1], color=colors[i], s=60, alpha=0.8, marker='s')
|
|
||||||
|
|
||||||
ax.set_xlabel('X Position (m)')
|
|
||||||
ax.set_ylabel('Y Position (m)')
|
|
||||||
ax.set_zlabel('Z Position (m)')
|
|
||||||
|
|
||||||
title = 'Orbital Trajectories (3D)'
|
|
||||||
if center_body:
|
|
||||||
title += f' - Centered on {center_body}'
|
|
||||||
ax.set_title(title)
|
|
||||||
|
|
||||||
ax.legend()
|
|
||||||
|
|
||||||
# Make axes equal
|
|
||||||
max_range = 0
|
|
||||||
for positions in trajectories.values():
|
|
||||||
range_val = np.max(np.abs(positions))
|
|
||||||
max_range = max(max_range, range_val)
|
|
||||||
|
|
||||||
ax.set_xlim(-max_range, max_range)
|
|
||||||
ax.set_ylim(-max_range, max_range)
|
|
||||||
ax.set_zlim(-max_range, max_range)
|
|
||||||
|
|
||||||
def plot_energy_over_time(snapshots, times):
|
|
||||||
"""Plot energy evolution over time."""
|
|
||||||
plt.figure(figsize=(12, 6))
|
|
||||||
|
|
||||||
total_energies = []
|
|
||||||
kinetic_energies = []
|
|
||||||
|
|
||||||
for snapshot in snapshots:
|
|
||||||
ke = 0
|
|
||||||
pe = 0
|
|
||||||
bodies = snapshot['bodies']
|
|
||||||
|
|
||||||
# Calculate kinetic energy
|
|
||||||
for body in bodies:
|
|
||||||
v_squared = np.sum(body['velocity']**2)
|
|
||||||
ke += 0.5 * body['mass'] * v_squared
|
|
||||||
|
|
||||||
# Calculate potential energy (simplified, assuming G=1 in normalized units)
|
|
||||||
G = 6.67430e-11 # You might need to adjust this based on your normalization
|
|
||||||
for i in range(len(bodies)):
|
|
||||||
for j in range(i+1, len(bodies)):
|
|
||||||
r = np.linalg.norm(bodies[i]['position'] - bodies[j]['position'])
|
|
||||||
pe -= G * bodies[i]['mass'] * bodies[j]['mass'] / r
|
|
||||||
|
|
||||||
kinetic_energies.append(ke)
|
|
||||||
total_energies.append(ke + pe)
|
|
||||||
|
|
||||||
plt.plot(times, kinetic_energies, label='Kinetic Energy', alpha=0.8)
|
|
||||||
plt.plot(times, total_energies, label='Total Energy', alpha=0.8)
|
|
||||||
|
|
||||||
plt.xlabel('Time (s)')
|
|
||||||
plt.ylabel('Energy (J)')
|
|
||||||
plt.title('Energy Conservation Over Time')
|
|
||||||
plt.legend()
|
|
||||||
plt.grid(True, alpha=0.3)
|
|
||||||
plt.tight_layout()
|
|
||||||
|
|
||||||
def plot_animated_2d(trajectories, times, interval=50, center_body=None):
|
|
||||||
"""Create animated 2D trajectory plot."""
|
|
||||||
fig, ax = plt.subplots(figsize=(12, 10))
|
|
||||||
|
|
||||||
colors = plt.cm.tab10(np.linspace(0, 1, len(trajectories)))
|
|
||||||
body_names = list(trajectories.keys())
|
|
||||||
|
|
||||||
# Set up the plot
|
|
||||||
ax.set_xlabel('X Position (m)')
|
|
||||||
ax.set_ylabel('Y Position (m)')
|
|
||||||
|
|
||||||
title = 'Animated Orbital Trajectories (X-Y Plane)'
|
|
||||||
if center_body:
|
|
||||||
title += f' - Centered on {center_body}'
|
|
||||||
ax.set_title(title)
|
|
||||||
ax.grid(True, alpha=0.3)
|
|
||||||
|
|
||||||
# Calculate plot limits
|
|
||||||
all_positions = np.concatenate(list(trajectories.values()))
|
|
||||||
margin = 0.1
|
|
||||||
x_range = np.max(all_positions[:, 0]) - np.min(all_positions[:, 0])
|
|
||||||
y_range = np.max(all_positions[:, 1]) - np.min(all_positions[:, 1])
|
|
||||||
x_center = (np.max(all_positions[:, 0]) + np.min(all_positions[:, 0])) / 2
|
|
||||||
y_center = (np.max(all_positions[:, 1]) + np.min(all_positions[:, 1])) / 2
|
|
||||||
|
|
||||||
max_range = max(x_range, y_range) * (1 + margin)
|
|
||||||
ax.set_xlim(x_center - max_range/2, x_center + max_range/2)
|
|
||||||
ax.set_ylim(y_center - max_range/2, y_center + max_range/2)
|
|
||||||
ax.set_aspect('equal')
|
|
||||||
|
|
||||||
# Initialize plot elements
|
|
||||||
trajectory_lines = []
|
|
||||||
body_points = []
|
|
||||||
body_trails = []
|
|
||||||
|
|
||||||
for i, name in enumerate(body_names):
|
|
||||||
# Trajectory line (will grow over time)
|
|
||||||
line, = ax.plot([], [], color=colors[i], alpha=0.7, linewidth=1.5, label=name)
|
|
||||||
trajectory_lines.append(line)
|
|
||||||
|
|
||||||
# Current body position
|
|
||||||
point, = ax.plot([], [], 'o', color=colors[i], markersize=8, alpha=0.9)
|
|
||||||
body_points.append(point)
|
|
||||||
|
|
||||||
# Trail of recent positions
|
|
||||||
trail, = ax.plot([], [], 'o', color=colors[i], markersize=3, alpha=0.3)
|
|
||||||
body_trails.append(trail)
|
|
||||||
|
|
||||||
# Time display
|
|
||||||
time_text = ax.text(0.02, 0.98, '', transform=ax.transAxes, fontsize=12,
|
|
||||||
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
|
|
||||||
|
|
||||||
ax.legend(loc='upper right')
|
|
||||||
|
|
||||||
def animate(frame):
|
|
||||||
current_time = times[frame]
|
|
||||||
|
|
||||||
# Format time for display
|
|
||||||
if current_time >= 86400: # More than a day
|
|
||||||
time_str = f"Time: {current_time/86400:.1f} days"
|
|
||||||
elif current_time >= 3600: # More than an hour
|
|
||||||
time_str = f"Time: {current_time/3600:.1f} hours"
|
|
||||||
else:
|
|
||||||
time_str = f"Time: {current_time:.1f} seconds"
|
|
||||||
|
|
||||||
time_text.set_text(time_str)
|
|
||||||
|
|
||||||
# Update each body
|
|
||||||
for i, name in enumerate(body_names):
|
|
||||||
positions = trajectories[name]
|
|
||||||
|
|
||||||
# Update trajectory line (show path up to current time)
|
|
||||||
x_data = positions[:frame+1, 0]
|
|
||||||
y_data = positions[:frame+1, 1]
|
|
||||||
trajectory_lines[i].set_data(x_data, y_data)
|
|
||||||
|
|
||||||
# Update current position
|
|
||||||
if frame < len(positions):
|
|
||||||
current_pos = positions[frame]
|
|
||||||
body_points[i].set_data([current_pos[0]], [current_pos[1]])
|
|
||||||
|
|
||||||
# Update trail (last 20 positions)
|
|
||||||
trail_start = max(0, frame - 20)
|
|
||||||
trail_x = positions[trail_start:frame, 0]
|
|
||||||
trail_y = positions[trail_start:frame, 1]
|
|
||||||
body_trails[i].set_data(trail_x, trail_y)
|
|
||||||
|
|
||||||
return trajectory_lines + body_points + body_trails + [time_text]
|
|
||||||
|
|
||||||
num_frames = len(times)
|
|
||||||
anim = animation.FuncAnimation(fig, animate, frames=num_frames,
|
|
||||||
interval=interval, blit=True, repeat=True)
|
|
||||||
|
|
||||||
return fig, anim
|
|
||||||
|
|
||||||
def plot_animated_3d(trajectories, times, interval=50, center_body=None):
|
|
||||||
"""Create animated 3D trajectory plot."""
|
|
||||||
fig = plt.figure(figsize=(12, 10))
|
|
||||||
ax = fig.add_subplot(111, projection='3d')
|
|
||||||
|
|
||||||
colors = plt.cm.tab10(np.linspace(0, 1, len(trajectories)))
|
|
||||||
body_names = list(trajectories.keys())
|
|
||||||
|
|
||||||
# Set up the plot
|
|
||||||
ax.set_xlabel('X Position (m)')
|
|
||||||
ax.set_ylabel('Y Position (m)')
|
|
||||||
ax.set_zlabel('Z Position (m)')
|
|
||||||
|
|
||||||
title = 'Animated Orbital Trajectories (3D)'
|
|
||||||
if center_body:
|
|
||||||
title += f' - Centered on {center_body}'
|
|
||||||
ax.set_title(title)
|
|
||||||
|
|
||||||
# Calculate plot limits
|
|
||||||
all_positions = np.concatenate(list(trajectories.values()))
|
|
||||||
max_range = np.max(np.abs(all_positions)) * 1.1
|
|
||||||
ax.set_xlim(-max_range, max_range)
|
|
||||||
ax.set_ylim(-max_range, max_range)
|
|
||||||
ax.set_zlim(-max_range, max_range)
|
|
||||||
|
|
||||||
# Initialize plot elements
|
|
||||||
trajectory_lines = []
|
|
||||||
body_points = []
|
|
||||||
|
|
||||||
for i, name in enumerate(body_names):
|
|
||||||
# Trajectory line
|
|
||||||
line, = ax.plot([], [], [], color=colors[i], alpha=0.7, linewidth=1.5, label=name)
|
|
||||||
trajectory_lines.append(line)
|
|
||||||
|
|
||||||
# Current body position
|
|
||||||
point = ax.scatter([], [], [], color=colors[i], s=100, alpha=0.9)
|
|
||||||
body_points.append(point)
|
|
||||||
|
|
||||||
# Time display
|
|
||||||
time_text = ax.text2D(0.02, 0.98, '', transform=ax.transAxes, fontsize=12,
|
|
||||||
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
|
|
||||||
|
|
||||||
ax.legend(loc='upper right')
|
|
||||||
|
|
||||||
def animate(frame):
|
|
||||||
current_time = times[frame]
|
|
||||||
|
|
||||||
# Format time for display
|
|
||||||
if current_time >= 86400: # More than a day
|
|
||||||
time_str = f"Time: {current_time/86400:.1f} days"
|
|
||||||
elif current_time >= 3600: # More than an hour
|
|
||||||
time_str = f"Time: {current_time/3600:.1f} hours"
|
|
||||||
else:
|
|
||||||
time_str = f"Time: {current_time:.1f} seconds"
|
|
||||||
|
|
||||||
time_text.set_text(time_str)
|
|
||||||
|
|
||||||
# Update each body
|
|
||||||
for i, name in enumerate(body_names):
|
|
||||||
positions = trajectories[name]
|
|
||||||
|
|
||||||
# Update trajectory line
|
|
||||||
x_data = positions[:frame+1, 0]
|
|
||||||
y_data = positions[:frame+1, 1]
|
|
||||||
z_data = positions[:frame+1, 2]
|
|
||||||
trajectory_lines[i].set_data(x_data, y_data)
|
|
||||||
trajectory_lines[i].set_3d_properties(z_data)
|
|
||||||
|
|
||||||
# Update current position
|
|
||||||
if frame < len(positions):
|
|
||||||
current_pos = positions[frame]
|
|
||||||
# Remove old scatter point and create new one
|
|
||||||
body_points[i].remove()
|
|
||||||
body_points[i] = ax.scatter([current_pos[0]], [current_pos[1]], [current_pos[2]],
|
|
||||||
color=colors[i], s=100, alpha=0.9)
|
|
||||||
|
|
||||||
return trajectory_lines + body_points + [time_text]
|
|
||||||
|
|
||||||
num_frames = len(times)
|
|
||||||
anim = animation.FuncAnimation(fig, animate, frames=num_frames,
|
|
||||||
interval=interval, blit=False, repeat=True)
|
|
||||||
|
|
||||||
return fig, anim
|
|
||||||
|
|
||||||
def calculate_animation_params(num_frames, target_duration_sec=60, manual_interval=None):
|
|
||||||
"""Calculate animation parameters for optimal viewing experience."""
|
|
||||||
if manual_interval is not None:
|
|
||||||
# User specified manual interval
|
|
||||||
interval_ms = manual_interval
|
|
||||||
total_duration_sec = num_frames * interval_ms / 1000.0
|
|
||||||
time_scale_factor = target_duration_sec / total_duration_sec
|
|
||||||
return interval_ms, total_duration_sec, time_scale_factor, True
|
|
||||||
|
|
||||||
# Auto-calculate interval for target duration
|
|
||||||
target_duration_ms = target_duration_sec * 1000
|
|
||||||
optimal_interval = max(10, target_duration_ms // num_frames) # Minimum 10ms for smooth animation
|
|
||||||
actual_duration_sec = num_frames * optimal_interval / 1000.0
|
|
||||||
time_scale_factor = target_duration_sec / actual_duration_sec
|
|
||||||
|
|
||||||
return optimal_interval, actual_duration_sec, time_scale_factor, False
|
|
||||||
|
|
||||||
def center_trajectories_on_body(trajectories, center_body_name):
|
|
||||||
"""Center all trajectories relative to the specified body."""
|
|
||||||
if center_body_name not in trajectories:
|
|
||||||
available_bodies = list(trajectories.keys())
|
|
||||||
raise ValueError(f"Body '{center_body_name}' not found. Available bodies: {available_bodies}")
|
|
||||||
|
|
||||||
center_trajectory = trajectories[center_body_name]
|
|
||||||
centered_trajectories = {}
|
|
||||||
|
|
||||||
for body_name, trajectory in trajectories.items():
|
|
||||||
# Subtract the center body's position from each body's trajectory
|
|
||||||
centered_trajectories[body_name] = trajectory - center_trajectory
|
|
||||||
|
|
||||||
return centered_trajectories
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description='Plot orbital trajectories from binary file')
|
|
||||||
parser.add_argument('trajectory_file', help='Binary trajectory file to plot')
|
|
||||||
parser.add_argument('--2d-only', action='store_true', dest='two_d_only', help='Only show 2D plot')
|
|
||||||
parser.add_argument('--3d-only', action='store_true', dest='three_d_only', help='Only show 3D plot')
|
|
||||||
parser.add_argument('--energy', action='store_true', help='Show energy plot')
|
|
||||||
parser.add_argument('--animate', action='store_true', help='Show animated trajectories')
|
|
||||||
parser.add_argument('--save-animation', type=str, help='Save animation as MP4 file')
|
|
||||||
parser.add_argument('--static', action='store_true', help='Show static plots (default if no --animate)')
|
|
||||||
parser.add_argument('--interval', type=int, help='Animation interval in milliseconds (default: auto-scaled to ~60s total)')
|
|
||||||
parser.add_argument('--target-duration', type=int, default=60, help='Target animation duration in seconds (default: 60)')
|
|
||||||
parser.add_argument('--center', type=str, help='Center animation on specified body (e.g., "Sun", "Earth")')
|
|
||||||
parser.add_argument('--list-bodies', action='store_true', help='List available bodies and exit')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
print(f"Reading trajectory file: {args.trajectory_file}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
snapshots = read_trajectory_file(args.trajectory_file)
|
|
||||||
print(f"Loaded {len(snapshots)} snapshots")
|
|
||||||
|
|
||||||
if not snapshots:
|
|
||||||
print("No data found in file!")
|
|
||||||
return
|
|
||||||
|
|
||||||
trajectories, times = organize_trajectories(snapshots)
|
|
||||||
|
|
||||||
# Handle list-bodies option
|
|
||||||
if args.list_bodies:
|
|
||||||
print(f"Available bodies in trajectory file:")
|
|
||||||
for body_name in sorted(trajectories.keys()):
|
|
||||||
print(f" - {body_name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Bodies found: {list(trajectories.keys())}")
|
|
||||||
print(f"Time range: {times[0]:.2e} - {times[-1]:.2e} seconds")
|
|
||||||
print(f"Number of time steps: {len(times)}")
|
|
||||||
|
|
||||||
# Center trajectories on specified body if requested
|
|
||||||
original_trajectories = trajectories.copy()
|
|
||||||
if args.center:
|
|
||||||
try:
|
|
||||||
trajectories = center_trajectories_on_body(trajectories, args.center)
|
|
||||||
print(f"🎯 Centering animation on: {args.center}")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"❌ Error: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if we should animate or show static plots
|
|
||||||
show_animation = args.animate or args.save_animation
|
|
||||||
|
|
||||||
if show_animation:
|
|
||||||
# Calculate animation parameters
|
|
||||||
interval_ms, anim_duration_sec, time_scale, is_manual = calculate_animation_params(
|
|
||||||
len(times), args.target_duration, args.interval
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"\n🎬 Animation Settings:")
|
|
||||||
print(f" Total frames: {len(times)}")
|
|
||||||
print(f" Animation duration: {anim_duration_sec:.1f} seconds")
|
|
||||||
print(f" Frame interval: {interval_ms}ms")
|
|
||||||
if is_manual:
|
|
||||||
print(f" ⚙️ Using manual interval (--interval {args.interval})")
|
|
||||||
if time_scale != 1.0:
|
|
||||||
print(f" ⏱️ Time scale: {time_scale:.2f}x (animation {'faster' if time_scale > 1 else 'slower'} than target)")
|
|
||||||
else:
|
|
||||||
print(f" 🤖 Auto-scaled for {args.target_duration}s target duration")
|
|
||||||
print(f" ⏱️ Time scale: 1.0x (optimized)")
|
|
||||||
|
|
||||||
simulation_duration = times[-1] - times[0]
|
|
||||||
if simulation_duration > 0:
|
|
||||||
compression_ratio = simulation_duration / anim_duration_sec
|
|
||||||
if compression_ratio >= 86400:
|
|
||||||
print(f" 📈 Compression: {compression_ratio/86400:.1f} days of simulation per second of animation")
|
|
||||||
elif compression_ratio >= 3600:
|
|
||||||
print(f" 📈 Compression: {compression_ratio/3600:.1f} hours of simulation per second of animation")
|
|
||||||
else:
|
|
||||||
print(f" 📈 Compression: {compression_ratio:.1f}x real-time")
|
|
||||||
print()
|
|
||||||
|
|
||||||
print("Creating animated plots...")
|
|
||||||
animations = []
|
|
||||||
|
|
||||||
# Create animated plots
|
|
||||||
if not args.three_d_only:
|
|
||||||
print("Creating 2D animation...")
|
|
||||||
fig_2d, anim_2d = plot_animated_2d(trajectories, times, interval_ms, args.center)
|
|
||||||
animations.append((fig_2d, anim_2d, '2d'))
|
|
||||||
|
|
||||||
if not args.two_d_only:
|
|
||||||
print("Creating 3D animation...")
|
|
||||||
fig_3d, anim_3d = plot_animated_3d(trajectories, times, interval_ms, args.center)
|
|
||||||
animations.append((fig_3d, anim_3d, '3d'))
|
|
||||||
|
|
||||||
# Save animations if requested
|
|
||||||
if args.save_animation:
|
|
||||||
for fig, anim, plot_type in animations:
|
|
||||||
filename = f"{args.save_animation}_{plot_type}.mp4"
|
|
||||||
print(f"Saving {plot_type.upper()} animation to {filename}...")
|
|
||||||
try:
|
|
||||||
anim.save(filename, writer='ffmpeg', fps=20)
|
|
||||||
print(f"Animation saved to {filename}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error saving animation: {e}")
|
|
||||||
print("Note: You may need to install ffmpeg for video export")
|
|
||||||
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
else:
|
|
||||||
print("Creating static plots...")
|
|
||||||
# Use original trajectories for static plots unless centering is requested
|
|
||||||
plot_trajectories = trajectories if args.center else original_trajectories
|
|
||||||
|
|
||||||
# Plot static trajectories
|
|
||||||
if not args.three_d_only:
|
|
||||||
plot_trajectories_2d(plot_trajectories, times, args.center)
|
|
||||||
|
|
||||||
if not args.two_d_only:
|
|
||||||
plot_trajectories_3d(plot_trajectories, times, args.center)
|
|
||||||
|
|
||||||
if args.energy:
|
|
||||||
plot_energy_over_time(snapshots, times)
|
|
||||||
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(f"Error: File '{args.trajectory_file}' not found!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error reading file: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
@ -1,3 +0,0 @@
|
|||||||
matplotlib>=3.5.0
|
|
||||||
numpy>=1.20.0
|
|
||||||
ffmpeg-python>=0.2.0
|
|
@ -1,24 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "orbital-simulator-gui"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Desktop GUI for Orbital Simulator"
|
|
||||||
authors = ["Thomas Faour"]
|
|
||||||
license = "MIT"
|
|
||||||
repository = ""
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
tauri-build = { version = "1.0", features = [] }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tauri = { version = "1.0", features = ["api-all"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
# by default Tauri runs in production mode
|
|
||||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
|
||||||
default = ["custom-protocol"]
|
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
|
@ -1,106 +0,0 @@
|
|||||||
#![cfg_attr(
|
|
||||||
all(not(debug_assertions), target_os = "windows"),
|
|
||||||
windows_subsystem = "windows"
|
|
||||||
)]
|
|
||||||
|
|
||||||
use tauri::{command, Context, generate_handler, generate_context};
|
|
||||||
use std::process::Command;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct SimulationConfig {
|
|
||||||
name: String,
|
|
||||||
path: String,
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
async fn get_available_configs() -> Result<Vec<SimulationConfig>, String> {
|
|
||||||
let configs = vec![
|
|
||||||
SimulationConfig {
|
|
||||||
name: "Earth-Sun System".to_string(),
|
|
||||||
path: "config/earthsun_corrected.toml".to_string(),
|
|
||||||
description: "Simple two-body system for testing".to_string(),
|
|
||||||
},
|
|
||||||
SimulationConfig {
|
|
||||||
name: "Inner Solar System".to_string(),
|
|
||||||
path: "config/inner_solar_system.toml".to_string(),
|
|
||||||
description: "Mercury through Mars plus Moon".to_string(),
|
|
||||||
},
|
|
||||||
SimulationConfig {
|
|
||||||
name: "Complete Solar System".to_string(),
|
|
||||||
path: "config/planets.toml".to_string(),
|
|
||||||
description: "All planets plus major moons".to_string(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
Ok(configs)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
async fn start_api_server() -> Result<String, String> {
|
|
||||||
// Start the API server in the background
|
|
||||||
let mut cmd = Command::new("cargo");
|
|
||||||
cmd.args(&["run", "--release", "--bin", "api_server"]);
|
|
||||||
|
|
||||||
match cmd.spawn() {
|
|
||||||
Ok(_) => Ok("API server started on http://localhost:4395".to_string()),
|
|
||||||
Err(e) => Err(format!("Failed to start API server: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
async fn run_simulation(
|
|
||||||
config_path: String,
|
|
||||||
time_str: String,
|
|
||||||
step_size: f64,
|
|
||||||
output_file: String,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let mut cmd = Command::new("cargo");
|
|
||||||
cmd.args(&[
|
|
||||||
"run",
|
|
||||||
"--release",
|
|
||||||
"--bin",
|
|
||||||
"simulator",
|
|
||||||
"--",
|
|
||||||
"--config", &config_path,
|
|
||||||
"--time", &time_str,
|
|
||||||
"--step-size", &step_size.to_string(),
|
|
||||||
"--output-file", &output_file,
|
|
||||||
"--force-overwrite",
|
|
||||||
]);
|
|
||||||
|
|
||||||
match cmd.output() {
|
|
||||||
Ok(output) => {
|
|
||||||
if output.status.success() {
|
|
||||||
Ok(format!("Simulation completed: {}", output_file))
|
|
||||||
} else {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
Err(format!("Simulation failed: {}", stderr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => Err(format!("Failed to run simulation: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
async fn visualize_trajectory(trajectory_file: String) -> Result<String, String> {
|
|
||||||
let mut cmd = Command::new("python3");
|
|
||||||
cmd.args(&["plot_trajectories.py", &trajectory_file, "--animate"]);
|
|
||||||
|
|
||||||
match cmd.spawn() {
|
|
||||||
Ok(_) => Ok("Visualization started".to_string()),
|
|
||||||
Err(e) => Err(format!("Failed to start visualization: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
tauri::Builder::default()
|
|
||||||
.invoke_handler(generate_handler![
|
|
||||||
get_available_configs,
|
|
||||||
start_api_server,
|
|
||||||
run_simulation,
|
|
||||||
visualize_trajectory
|
|
||||||
])
|
|
||||||
.run(generate_context!())
|
|
||||||
.expect("error while running tauri application");
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
{
|
|
||||||
"build": {
|
|
||||||
"beforeDevCommand": "cd web && npm run dev",
|
|
||||||
"beforeBuildCommand": "cd web && npm run build",
|
|
||||||
"devPath": "http://localhost:5173",
|
|
||||||
"distDir": "../web/dist",
|
|
||||||
"withGlobalTauri": false
|
|
||||||
},
|
|
||||||
"package": {
|
|
||||||
"productName": "Orbital Simulator",
|
|
||||||
"version": "0.1.0"
|
|
||||||
},
|
|
||||||
"tauri": {
|
|
||||||
"allowlist": {
|
|
||||||
"all": false,
|
|
||||||
"shell": {
|
|
||||||
"all": false,
|
|
||||||
"open": true
|
|
||||||
},
|
|
||||||
"dialog": {
|
|
||||||
"all": false,
|
|
||||||
"open": true,
|
|
||||||
"save": true
|
|
||||||
},
|
|
||||||
"fs": {
|
|
||||||
"all": false,
|
|
||||||
"readFile": true,
|
|
||||||
"writeFile": true,
|
|
||||||
"readDir": true,
|
|
||||||
"copyFile": true,
|
|
||||||
"createDir": true,
|
|
||||||
"removeDir": true,
|
|
||||||
"removeFile": true,
|
|
||||||
"renameFile": true,
|
|
||||||
"exists": true
|
|
||||||
},
|
|
||||||
"path": {
|
|
||||||
"all": true
|
|
||||||
},
|
|
||||||
"window": {
|
|
||||||
"all": false,
|
|
||||||
"close": true,
|
|
||||||
"hide": true,
|
|
||||||
"show": true,
|
|
||||||
"maximize": true,
|
|
||||||
"minimize": true,
|
|
||||||
"unmaximize": true,
|
|
||||||
"unminimize": true,
|
|
||||||
"startDragging": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bundle": {
|
|
||||||
"active": true,
|
|
||||||
"targets": "all",
|
|
||||||
"identifier": "com.orbital-simulator.app",
|
|
||||||
"icon": [
|
|
||||||
"icons/32x32.png",
|
|
||||||
"icons/128x128.png",
|
|
||||||
"icons/128x128@2x.png",
|
|
||||||
"icons/icon.icns",
|
|
||||||
"icons/icon.ico"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"security": {
|
|
||||||
"csp": null
|
|
||||||
},
|
|
||||||
"windows": [
|
|
||||||
{
|
|
||||||
"fullscreen": false,
|
|
||||||
"resizable": true,
|
|
||||||
"title": "Orbital Simulator",
|
|
||||||
"width": 1400,
|
|
||||||
"height": 900,
|
|
||||||
"minWidth": 800,
|
|
||||||
"minHeight": 600
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,520 +0,0 @@
|
|||||||
use axum::{
|
|
||||||
extract::{Path, Query, State},
|
|
||||||
http::{StatusCode, HeaderMap},
|
|
||||||
response::Json,
|
|
||||||
routing::{get, post},
|
|
||||||
Router,
|
|
||||||
};
|
|
||||||
use dashmap::DashMap;
|
|
||||||
use orbital_simulator::config::Config;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
thread,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
use tokio::sync::broadcast;
|
|
||||||
use tower_http::cors::CorsLayer;
|
|
||||||
use tower_http::services::ServeDir;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AppState {
|
|
||||||
pub simulations: Arc<DashMap<Uuid, SimulationSession>>,
|
|
||||||
pub broadcasters: Arc<DashMap<Uuid, broadcast::Sender<SimulationUpdate>>>,
|
|
||||||
pub user_sessions: Arc<DashMap<String, Vec<Uuid>>>, // session_id -> simulation_ids
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct SimulationSession {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub session_id: String,
|
|
||||||
pub config: Config,
|
|
||||||
pub step_size: f64,
|
|
||||||
pub total_steps: usize,
|
|
||||||
pub is_running: Arc<Mutex<bool>>,
|
|
||||||
pub current_step: Arc<Mutex<usize>>,
|
|
||||||
pub current_bodies: Arc<Mutex<Vec<orbital_simulator::config::Body>>>,
|
|
||||||
pub simulation_history: Arc<Mutex<Vec<SimulationUpdate>>>, // Store all simulation states
|
|
||||||
pub playback_step: Arc<Mutex<usize>>, // Current playback position (can be different from simulation step)
|
|
||||||
pub start_time: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
pub struct SimulationUpdate {
|
|
||||||
pub step: usize,
|
|
||||||
pub time: f64,
|
|
||||||
pub bodies: Vec<BodyState>,
|
|
||||||
pub energy: Option<EnergyInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
pub struct BodyState {
|
|
||||||
pub name: String,
|
|
||||||
pub position: [f64; 3],
|
|
||||||
pub velocity: [f64; 3],
|
|
||||||
pub mass: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
pub struct EnergyInfo {
|
|
||||||
pub kinetic: f64,
|
|
||||||
pub potential: f64,
|
|
||||||
pub total: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct CreateSimulationRequest {
|
|
||||||
pub config: Config,
|
|
||||||
pub step_size: f64,
|
|
||||||
pub total_steps: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct SimulationControlParams {
|
|
||||||
pub action: String, // "start", "pause", "stop", "step"
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct SeekParams {
|
|
||||||
pub step: usize, // Step to seek to
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct SimulationInfo {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub is_running: bool,
|
|
||||||
pub current_step: usize,
|
|
||||||
pub playback_step: usize, // Add playback position
|
|
||||||
pub total_steps: usize,
|
|
||||||
pub recorded_steps: usize, // How many steps have been simulated so far
|
|
||||||
pub bodies_count: usize,
|
|
||||||
pub elapsed_time: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to get or create session ID
|
|
||||||
fn get_session_id(headers: &HeaderMap) -> String {
|
|
||||||
headers
|
|
||||||
.get("x-session-id")
|
|
||||||
.and_then(|h| h.to_str().ok())
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.unwrap_or_else(|| Uuid::new_v4().to_string()) // Generate new session if none provided
|
|
||||||
}
|
|
||||||
|
|
||||||
// API Handlers
|
|
||||||
pub async fn create_simulation(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Json(request): Json<CreateSimulationRequest>,
|
|
||||||
) -> Result<Json<SimulationInfo>, StatusCode> {
|
|
||||||
let id = Uuid::new_v4();
|
|
||||||
let session_id = get_session_id(&headers);
|
|
||||||
|
|
||||||
let session = SimulationSession {
|
|
||||||
id,
|
|
||||||
session_id: session_id.clone(),
|
|
||||||
config: request.config.clone(),
|
|
||||||
step_size: request.step_size,
|
|
||||||
total_steps: request.total_steps,
|
|
||||||
is_running: Arc::new(Mutex::new(false)),
|
|
||||||
current_step: Arc::new(Mutex::new(0)),
|
|
||||||
current_bodies: Arc::new(Mutex::new(request.config.bodies.clone())),
|
|
||||||
simulation_history: Arc::new(Mutex::new(Vec::new())),
|
|
||||||
playback_step: Arc::new(Mutex::new(0)),
|
|
||||||
start_time: Instant::now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create broadcast channel for real-time updates
|
|
||||||
let (tx, _) = broadcast::channel(1000);
|
|
||||||
state.broadcasters.insert(id, tx);
|
|
||||||
|
|
||||||
// Add simulation to user's session
|
|
||||||
state.user_sessions.entry(session_id.clone())
|
|
||||||
.or_insert_with(Vec::new)
|
|
||||||
.push(id);
|
|
||||||
|
|
||||||
let info = SimulationInfo {
|
|
||||||
id,
|
|
||||||
is_running: false,
|
|
||||||
current_step: 0,
|
|
||||||
playback_step: 0,
|
|
||||||
total_steps: request.total_steps,
|
|
||||||
recorded_steps: 0,
|
|
||||||
bodies_count: request.config.bodies.len(),
|
|
||||||
elapsed_time: 0.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
state.simulations.insert(id, session);
|
|
||||||
|
|
||||||
Ok(Json(info))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_simulations(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
) -> Json<Vec<SimulationInfo>> {
|
|
||||||
let session_id = get_session_id(&headers);
|
|
||||||
|
|
||||||
let simulations: Vec<SimulationInfo> = if let Some(sim_ids) = state.user_sessions.get(&session_id) {
|
|
||||||
sim_ids.iter()
|
|
||||||
.filter_map(|sim_id| state.simulations.get(sim_id))
|
|
||||||
.map(|entry| {
|
|
||||||
let session = entry.value();
|
|
||||||
SimulationInfo {
|
|
||||||
id: session.id,
|
|
||||||
is_running: *session.is_running.lock().unwrap(),
|
|
||||||
current_step: *session.current_step.lock().unwrap(),
|
|
||||||
playback_step: *session.playback_step.lock().unwrap(),
|
|
||||||
total_steps: session.total_steps,
|
|
||||||
recorded_steps: session.simulation_history.lock().unwrap().len(),
|
|
||||||
bodies_count: session.config.bodies.len(),
|
|
||||||
elapsed_time: session.start_time.elapsed().as_secs_f64(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
Vec::new() // No simulations for this session
|
|
||||||
};
|
|
||||||
|
|
||||||
Json(simulations)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_simulation_state(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path(id): Path<Uuid>,
|
|
||||||
) -> Result<Json<SimulationUpdate>, StatusCode> {
|
|
||||||
let session_id = get_session_id(&headers);
|
|
||||||
let session = state.simulations.get(&id).ok_or(StatusCode::NOT_FOUND)?;
|
|
||||||
|
|
||||||
// Verify session ownership
|
|
||||||
if session.session_id != session_id {
|
|
||||||
return Err(StatusCode::FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
let playback_step = *session.playback_step.lock().unwrap();
|
|
||||||
let history = session.simulation_history.lock().unwrap();
|
|
||||||
|
|
||||||
// Return the state at the current playback position
|
|
||||||
if let Some(historical_state) = history.get(playback_step) {
|
|
||||||
Ok(Json(historical_state.clone()))
|
|
||||||
} else {
|
|
||||||
// If no history yet, return current live state
|
|
||||||
let step = *session.current_step.lock().unwrap();
|
|
||||||
let bodies = session.current_bodies.lock().unwrap().clone();
|
|
||||||
|
|
||||||
let body_states: Vec<BodyState> = session.config.bodies.iter().zip(bodies.iter()).map(|(config_body, sim_body)| {
|
|
||||||
BodyState {
|
|
||||||
name: config_body.name.clone(),
|
|
||||||
position: [sim_body.position.x, sim_body.position.y, sim_body.position.z],
|
|
||||||
velocity: [sim_body.velocity.x, sim_body.velocity.y, sim_body.velocity.z],
|
|
||||||
mass: sim_body.mass,
|
|
||||||
}
|
|
||||||
}).collect();
|
|
||||||
|
|
||||||
let update = SimulationUpdate {
|
|
||||||
step,
|
|
||||||
time: (step as f64) * session.step_size,
|
|
||||||
bodies: body_states,
|
|
||||||
energy: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Json(update))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn control_simulation(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path(id): Path<Uuid>,
|
|
||||||
Query(params): Query<SimulationControlParams>,
|
|
||||||
) -> Result<Json<SimulationInfo>, StatusCode> {
|
|
||||||
let session_id = get_session_id(&headers);
|
|
||||||
let session = state.simulations.get(&id).ok_or(StatusCode::NOT_FOUND)?;
|
|
||||||
|
|
||||||
// Verify session ownership
|
|
||||||
if session.session_id != session_id {
|
|
||||||
return Err(StatusCode::FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
match params.action.as_str() {
|
|
||||||
"start" => {
|
|
||||||
let mut is_running = session.is_running.lock().unwrap();
|
|
||||||
if !*is_running {
|
|
||||||
*is_running = true;
|
|
||||||
|
|
||||||
// Start simulation in background thread
|
|
||||||
let session_clone = session.clone();
|
|
||||||
let broadcaster = state.broadcasters.get(&id).unwrap().clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
run_simulation_loop(session_clone, broadcaster);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"pause" => {
|
|
||||||
let mut is_running = session.is_running.lock().unwrap();
|
|
||||||
*is_running = false;
|
|
||||||
}
|
|
||||||
"stop" => {
|
|
||||||
let mut is_running = session.is_running.lock().unwrap();
|
|
||||||
*is_running = false;
|
|
||||||
let mut step = session.current_step.lock().unwrap();
|
|
||||||
*step = 0;
|
|
||||||
}
|
|
||||||
"step" => {
|
|
||||||
// Single step execution - simplified for now
|
|
||||||
let mut step = session.current_step.lock().unwrap();
|
|
||||||
*step += 1;
|
|
||||||
}
|
|
||||||
_ => return Err(StatusCode::BAD_REQUEST),
|
|
||||||
}
|
|
||||||
|
|
||||||
let info = SimulationInfo {
|
|
||||||
id: session.id,
|
|
||||||
is_running: *session.is_running.lock().unwrap(),
|
|
||||||
current_step: *session.current_step.lock().unwrap(),
|
|
||||||
playback_step: *session.playback_step.lock().unwrap(),
|
|
||||||
total_steps: session.total_steps,
|
|
||||||
recorded_steps: session.simulation_history.lock().unwrap().len(),
|
|
||||||
bodies_count: session.config.bodies.len(),
|
|
||||||
elapsed_time: session.start_time.elapsed().as_secs_f64(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Json(info))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_simulation(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path(id): Path<Uuid>,
|
|
||||||
) -> Result<StatusCode, StatusCode> {
|
|
||||||
let session_id = get_session_id(&headers);
|
|
||||||
|
|
||||||
if let Some((_, session)) = state.simulations.remove(&id) {
|
|
||||||
// Verify session ownership
|
|
||||||
if session.session_id != session_id {
|
|
||||||
// Put it back if not owned by this session
|
|
||||||
state.simulations.insert(id, session);
|
|
||||||
return Err(StatusCode::FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the simulation
|
|
||||||
let mut is_running = session.is_running.lock().unwrap();
|
|
||||||
*is_running = false;
|
|
||||||
|
|
||||||
// Remove broadcaster
|
|
||||||
state.broadcasters.remove(&id);
|
|
||||||
|
|
||||||
// Remove from user's session
|
|
||||||
if let Some(mut sim_ids) = state.user_sessions.get_mut(&session_id) {
|
|
||||||
sim_ids.retain(|&sim_id| sim_id != id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(StatusCode::NO_CONTENT)
|
|
||||||
} else {
|
|
||||||
Err(StatusCode::NOT_FOUND)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_available_configs() -> Json<Vec<String>> {
|
|
||||||
// Return list of available configuration files
|
|
||||||
let configs = vec![
|
|
||||||
"planets.toml".to_string(),
|
|
||||||
"solar_system.toml".to_string(),
|
|
||||||
"inner_solar_system.toml".to_string(),
|
|
||||||
"earthsun_corrected.toml".to_string(),
|
|
||||||
];
|
|
||||||
Json(configs)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_simulation_loop(session: SimulationSession, broadcaster: broadcast::Sender<SimulationUpdate>) {
|
|
||||||
let target_fps = 60.0; // Increased from 30 FPS
|
|
||||||
let frame_duration = Duration::from_secs_f64(1.0 / target_fps);
|
|
||||||
let broadcast_every_n_steps = 1000; // Only broadcast every 2nd step to reduce overhead
|
|
||||||
|
|
||||||
// Create a high-performance physics simulation
|
|
||||||
let mut bodies = session.current_bodies.lock().unwrap().clone();
|
|
||||||
let mut step_counter = 0;
|
|
||||||
|
|
||||||
while *session.is_running.lock().unwrap() && step_counter < session.total_steps {
|
|
||||||
let start = Instant::now();
|
|
||||||
|
|
||||||
// High-performance gravitational simulation step
|
|
||||||
simulate_step(&mut bodies, session.step_size);
|
|
||||||
step_counter += 1;
|
|
||||||
|
|
||||||
// Update step counter (less frequently to reduce lock contention)
|
|
||||||
if step_counter % broadcast_every_n_steps == 0 {
|
|
||||||
let step = {
|
|
||||||
let mut step = session.current_step.lock().unwrap();
|
|
||||||
*step = step_counter;
|
|
||||||
*step
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update stored bodies (less frequently)
|
|
||||||
{
|
|
||||||
let mut stored_bodies = session.current_bodies.lock().unwrap();
|
|
||||||
*stored_bodies = bodies.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast update (less frequently to reduce serialization overhead)
|
|
||||||
let body_states: Vec<BodyState> = session.config.bodies.iter().zip(bodies.iter()).map(|(config_body, sim_body)| {
|
|
||||||
BodyState {
|
|
||||||
name: config_body.name.clone(),
|
|
||||||
position: [sim_body.position.x, sim_body.position.y, sim_body.position.z],
|
|
||||||
velocity: [sim_body.velocity.x, sim_body.velocity.y, sim_body.velocity.z],
|
|
||||||
mass: sim_body.mass,
|
|
||||||
}
|
|
||||||
}).collect();
|
|
||||||
|
|
||||||
let update = SimulationUpdate {
|
|
||||||
step,
|
|
||||||
time: (step as f64) * session.step_size,
|
|
||||||
bodies: body_states.clone(),
|
|
||||||
energy: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store in history for timeline playback
|
|
||||||
{
|
|
||||||
let mut history = session.simulation_history.lock().unwrap();
|
|
||||||
history.push(update.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update playback step to current step (auto-advance timeline)
|
|
||||||
{
|
|
||||||
let mut playback_step = session.playback_step.lock().unwrap();
|
|
||||||
*playback_step = step;
|
|
||||||
}
|
|
||||||
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
if elapsed < frame_duration {
|
|
||||||
thread::sleep(frame_duration - elapsed);
|
|
||||||
}
|
|
||||||
// Send update (ignore if no receivers)
|
|
||||||
let _ = broadcaster.send(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maintain target frame rate (but allow faster computation)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// High-performance physics simulation (back to SI units for compatibility)
|
|
||||||
fn simulate_step(bodies: &mut Vec<orbital_simulator::config::Body>, dt: f64) {
|
|
||||||
use glam::DVec3;
|
|
||||||
|
|
||||||
let n = bodies.len();
|
|
||||||
|
|
||||||
// Reset accelerations
|
|
||||||
for body in bodies.iter_mut() {
|
|
||||||
body.acceleration = DVec3::ZERO;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate gravitational forces (optimized: each pair calculated once)
|
|
||||||
// Using SI units to match input data (G = 6.67430e-11)
|
|
||||||
const G: f64 = 6.67430e-11;
|
|
||||||
|
|
||||||
for i in 0..(n-1) {
|
|
||||||
for j in (i+1)..n {
|
|
||||||
let r = bodies[j].position - bodies[i].position;
|
|
||||||
let r_mag = r.length();
|
|
||||||
if r_mag > 0.0 {
|
|
||||||
// Cache masses to avoid borrowing issues
|
|
||||||
let mass_i = bodies[i].mass;
|
|
||||||
let mass_j = bodies[j].mass;
|
|
||||||
|
|
||||||
// Gravitational force magnitude: F = G * m1 * m2 / r²
|
|
||||||
// Acceleration: a = F/m = G * other_mass / r²
|
|
||||||
let g_over_r3 = G / (r_mag * r_mag * r_mag);
|
|
||||||
let force_direction = r; // from i to j
|
|
||||||
|
|
||||||
// Newton's third law: equal and opposite forces
|
|
||||||
bodies[i].acceleration += mass_j * g_over_r3 * force_direction;
|
|
||||||
bodies[j].acceleration -= mass_i * g_over_r3 * force_direction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Velocity-Verlet integration (more stable than simple Euler)
|
|
||||||
for body in bodies.iter_mut() {
|
|
||||||
// Update position: x(t+dt) = x(t) + v(t)*dt + 0.5*a(t)*dt²
|
|
||||||
body.position += body.velocity * dt + 0.5 * body.acceleration * dt * dt;
|
|
||||||
// Update velocity: v(t+dt) = v(t) + a(t)*dt
|
|
||||||
body.velocity += body.acceleration * dt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn seek_simulation(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path(id): Path<Uuid>,
|
|
||||||
Query(params): Query<SeekParams>,
|
|
||||||
) -> Result<Json<SimulationUpdate>, StatusCode> {
|
|
||||||
let session_id = get_session_id(&headers);
|
|
||||||
let session = state.simulations.get(&id).ok_or(StatusCode::NOT_FOUND)?;
|
|
||||||
|
|
||||||
// Verify session ownership
|
|
||||||
if session.session_id != session_id {
|
|
||||||
return Err(StatusCode::FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
let history = session.simulation_history.lock().unwrap();
|
|
||||||
let recorded_steps = history.len();
|
|
||||||
|
|
||||||
// Find the closest recorded step to the requested step
|
|
||||||
if params.step < recorded_steps {
|
|
||||||
// Update playback position
|
|
||||||
{
|
|
||||||
let mut playback_step = session.playback_step.lock().unwrap();
|
|
||||||
*playback_step = params.step;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the historical state
|
|
||||||
Ok(Json(history[params.step].clone()))
|
|
||||||
} else if recorded_steps > 0 {
|
|
||||||
// If requesting beyond recorded history, return the latest recorded step
|
|
||||||
let latest_index = recorded_steps - 1;
|
|
||||||
{
|
|
||||||
let mut playback_step = session.playback_step.lock().unwrap();
|
|
||||||
*playback_step = latest_index;
|
|
||||||
}
|
|
||||||
Ok(Json(history[latest_index].clone()))
|
|
||||||
} else {
|
|
||||||
return Err(StatusCode::BAD_REQUEST);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_app() -> Router {
|
|
||||||
let state = AppState {
|
|
||||||
simulations: Arc::new(DashMap::new()),
|
|
||||||
broadcasters: Arc::new(DashMap::new()),
|
|
||||||
user_sessions: Arc::new(DashMap::new()),
|
|
||||||
};
|
|
||||||
|
|
||||||
Router::new()
|
|
||||||
.route("/api/simulations", post(create_simulation))
|
|
||||||
.route("/api/simulations", get(list_simulations))
|
|
||||||
.route("/api/simulations/:id", get(get_simulation_state))
|
|
||||||
.route("/api/simulations/:id/control", post(control_simulation))
|
|
||||||
.route("/api/simulations/:id/seek", post(seek_simulation))
|
|
||||||
.route("/api/simulations/:id", axum::routing::delete(delete_simulation))
|
|
||||||
.route("/api/configs", get(get_available_configs))
|
|
||||||
.layer(CorsLayer::permissive())
|
|
||||||
.with_state(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
pub async fn main() {
|
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
let app = create_app();
|
|
||||||
|
|
||||||
// Serve static files for the web frontend
|
|
||||||
let app = app.nest_service("/", ServeDir::new("web/dist"));
|
|
||||||
|
|
||||||
println!("🚀 Orbital Simulator API Server starting on http://localhost:4395");
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:4395").await.unwrap();
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
// use bevy::prelude::*;
|
|
||||||
// use bevy::input::mouse::{MouseMotion, MouseWheel};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// App::new()
|
|
||||||
// .add_plugins(DefaultPlugins)
|
|
||||||
// .add_systems(Startup, setup)
|
|
||||||
// .add_systems(Update, (camera_orbit_controls,))
|
|
||||||
// .run();
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[derive(Component)]
|
|
||||||
// struct CameraController {
|
|
||||||
// pub radius: f32,
|
|
||||||
// pub theta: f32, // azimuthal angle
|
|
||||||
// pub phi: f32, // polar angle
|
|
||||||
// pub _last_mouse_pos: Option<Vec2>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn setup(
|
|
||||||
// mut commands: Commands,
|
|
||||||
// mut meshes: ResMut<Assets<Mesh>>,
|
|
||||||
// mut materials: ResMut<Assets<StandardMaterial>>,
|
|
||||||
// ) {
|
|
||||||
// // Camera with controller
|
|
||||||
// let radius = 30.0;
|
|
||||||
// let theta = 0.0;
|
|
||||||
// let phi = std::f32::consts::FRAC_PI_4;
|
|
||||||
// let cam_pos = spherical_to_cartesian(radius, theta, phi);
|
|
||||||
// commands.spawn((
|
|
||||||
// Camera3dBundle {
|
|
||||||
// transform: Transform::from_translation(cam_pos).looking_at(Vec3::ZERO, Vec3::Y),
|
|
||||||
// ..default()
|
|
||||||
// },
|
|
||||||
// CameraController {
|
|
||||||
// radius,
|
|
||||||
// theta,
|
|
||||||
// phi,
|
|
||||||
// _last_mouse_pos: None,
|
|
||||||
// },
|
|
||||||
// ));
|
|
||||||
// // Light
|
|
||||||
// commands.spawn(PointLightBundle {
|
|
||||||
// point_light: PointLight {
|
|
||||||
// intensity: 1500.0,
|
|
||||||
// shadows_enabled: true,
|
|
||||||
// ..default()
|
|
||||||
// },
|
|
||||||
// transform: Transform::from_xyz(4.0, 8.0, 4.0),
|
|
||||||
// ..default()
|
|
||||||
// });
|
|
||||||
// // Add a sphere at the origin using the new Sphere primitive
|
|
||||||
// let mesh = meshes.add(Mesh::from(Sphere { radius: 1.0, ..default() }));
|
|
||||||
// let material = materials.add(StandardMaterial {
|
|
||||||
// base_color: Color::BLUE,
|
|
||||||
// ..default()
|
|
||||||
// });
|
|
||||||
// commands.spawn(PbrBundle {
|
|
||||||
// mesh,
|
|
||||||
// material,
|
|
||||||
// ..default()
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn spherical_to_cartesian(radius: f32, theta: f32, phi: f32) -> Vec3 {
|
|
||||||
// let x = radius * phi.sin() * theta.cos();
|
|
||||||
// let y = radius * phi.cos();
|
|
||||||
// let z = radius * phi.sin() * theta.sin();
|
|
||||||
// Vec3::new(x, y, z)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn camera_orbit_controls(
|
|
||||||
// mut query: Query<(&mut Transform, &mut CameraController)>,
|
|
||||||
// mut mouse_motion_events: EventReader<MouseMotion>,
|
|
||||||
// mut mouse_wheel_events: EventReader<MouseWheel>,
|
|
||||||
// mouse_button_input: Res<ButtonInput<MouseButton>>,
|
|
||||||
// windows: Query<&Window>,
|
|
||||||
// ) {
|
|
||||||
// let _window = if let Some(window) = windows.iter().next() { window } else { return; };
|
|
||||||
// let mut delta = Vec2::ZERO;
|
|
||||||
// for event in mouse_motion_events.read() {
|
|
||||||
// delta += event.delta;
|
|
||||||
// }
|
|
||||||
// let mut scroll = 0.0;
|
|
||||||
// for event in mouse_wheel_events.read() {
|
|
||||||
// scroll += event.y;
|
|
||||||
// }
|
|
||||||
// for (mut transform, mut controller) in query.iter_mut() {
|
|
||||||
// // Orbit (right mouse button)
|
|
||||||
// if mouse_button_input.pressed(MouseButton::Right) {
|
|
||||||
// let sensitivity = 0.01;
|
|
||||||
// controller.theta -= delta.x * sensitivity;
|
|
||||||
// controller.phi = (controller.phi - delta.y * sensitivity).clamp(0.05, std::f32::consts::PI - 0.05);
|
|
||||||
// }
|
|
||||||
// // Zoom
|
|
||||||
// if scroll.abs() > 0.0 {
|
|
||||||
// controller.radius = (controller.radius - scroll).clamp(3.0, 200.0);
|
|
||||||
// }
|
|
||||||
// // Update camera position
|
|
||||||
// let pos = spherical_to_cartesian(controller.radius, controller.theta, controller.phi);
|
|
||||||
// *transform = Transform::from_translation(pos).looking_at(Vec3::ZERO, Vec3::Y);
|
|
||||||
// }
|
|
||||||
// }
|
|
@ -1,164 +0,0 @@
|
|||||||
// Standard library
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fs;
|
|
||||||
use std::io::{self, Write};
|
|
||||||
use std::path::Path;
|
|
||||||
use std::time::{Duration,Instant};
|
|
||||||
|
|
||||||
// External crates
|
|
||||||
use clap::Parser;
|
|
||||||
use log::{info, debug};
|
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
|
||||||
use humantime;
|
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
// Internal modules from the library crate
|
|
||||||
use orbital_simulator::simulation::Simulation;
|
|
||||||
use orbital_simulator::types::{norm_time, real_time};
|
|
||||||
use orbital_simulator as _; // for mod resolution
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(
|
|
||||||
version,
|
|
||||||
about="Orbital mechanics simulator",
|
|
||||||
long_about = "Given initial conditions you provide to --config, \
|
|
||||||
this program will numerically integrate and determinate their \
|
|
||||||
paths based off Newton's law of gravity.")]
|
|
||||||
struct Args {
|
|
||||||
///Config file for initial conditions
|
|
||||||
#[arg(short, long)]
|
|
||||||
config: String,
|
|
||||||
|
|
||||||
/// Time to run simulation for (e.g. 10s, 5m, 2h, 100d)
|
|
||||||
#[arg(short, long, value_parser = humantime::parse_duration)]
|
|
||||||
time: Duration,
|
|
||||||
|
|
||||||
///Step size for simulation (seconds)
|
|
||||||
#[arg(short, long, default_value_t = 10.0)]
|
|
||||||
step_size: f64,
|
|
||||||
|
|
||||||
///How often to update progress bar and save (seconds)
|
|
||||||
#[arg(short = 'P', long, default_value_t = 1000)]
|
|
||||||
steps_per_save: usize,
|
|
||||||
|
|
||||||
/// Filename to save trajectory to
|
|
||||||
#[arg(short, long)]
|
|
||||||
output_file: String,
|
|
||||||
|
|
||||||
/// Force overwrite existing output file without confirmation
|
|
||||||
#[arg(short = 'w', long)]
|
|
||||||
force_overwrite: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_config<P: AsRef<Path>>(path: P) -> Result<orbital_simulator::config::Config, Box<dyn Error>> {
|
|
||||||
let content = fs::read_to_string(&path)?;
|
|
||||||
let path_str = path.as_ref().to_string_lossy();
|
|
||||||
|
|
||||||
let conf: orbital_simulator::config::Config = if path_str.ends_with(".json") {
|
|
||||||
serde_json::from_str(&content)?
|
|
||||||
} else if path_str.ends_with(".toml") {
|
|
||||||
toml::from_str(&content)?
|
|
||||||
} else {
|
|
||||||
// Try JSON first, then TOML
|
|
||||||
serde_json::from_str(&content).or_else(|_| toml::from_str(&content))?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply normalization settings if present
|
|
||||||
conf.apply_normalization();
|
|
||||||
|
|
||||||
Ok(conf)
|
|
||||||
}
|
|
||||||
//fn parse_time(arg: &str)
|
|
||||||
|
|
||||||
fn format_duration_single_unit(dur: Duration) -> String {
|
|
||||||
let secs = dur.as_secs_f64();
|
|
||||||
|
|
||||||
const MINUTE: f64 = 60.0;
|
|
||||||
const HOUR: f64 = 60.0 * MINUTE;
|
|
||||||
const DAY: f64 = 24.0 * HOUR;
|
|
||||||
const YEAR: f64 = 365.25 * DAY;
|
|
||||||
|
|
||||||
if secs >= YEAR {
|
|
||||||
format!("{:.0} years", (secs / YEAR).round())
|
|
||||||
} else if secs >= DAY {
|
|
||||||
format!("{:.0} days", (secs / DAY).round())
|
|
||||||
} else if secs >= HOUR {
|
|
||||||
format!("{:.0} hours", (secs / HOUR).round())
|
|
||||||
} else if secs >= MINUTE {
|
|
||||||
format!("{:.0} minutes", (secs / MINUTE).round())
|
|
||||||
} else {
|
|
||||||
format!("{:.0} seconds", secs.round())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_output_file(path: &str, force_overwrite: bool) -> Result<(), Box<dyn Error>> {
|
|
||||||
if Path::new(path).exists() {
|
|
||||||
if force_overwrite {
|
|
||||||
info!("Overwriting existing file: {}", path);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
print!("⚠️ Output file '{}' already exists. Overwrite? [y/N]: ", path);
|
|
||||||
io::stdout().flush()?;
|
|
||||||
|
|
||||||
let mut input = String::new();
|
|
||||||
io::stdin().read_line(&mut input)?;
|
|
||||||
|
|
||||||
let response = input.trim().to_lowercase();
|
|
||||||
if response == "y" || response == "yes" {
|
|
||||||
info!("Overwriting existing file: {}", path);
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
println!("❌ Aborted. Use --force-overwrite (-w) to skip confirmation.");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
env_logger::init();
|
|
||||||
let args = Args::parse();
|
|
||||||
|
|
||||||
info!("Loading initial parameters from {}", args.config);
|
|
||||||
|
|
||||||
let conf = read_config(args.config).unwrap();
|
|
||||||
|
|
||||||
// Check if output file exists and handle overwrite
|
|
||||||
check_output_file(&args.output_file, args.force_overwrite).unwrap();
|
|
||||||
|
|
||||||
for body in &conf.bodies {
|
|
||||||
info!("Loaded {} with mass {:.3e} kg", body.name, body.mass);
|
|
||||||
debug!("R_i = {:?}, V_i = {:?}", body.position, body.velocity);
|
|
||||||
}
|
|
||||||
debug!("Simulation time and step size: {} seconds, {} seconds per step",
|
|
||||||
args.time.as_secs(), args.step_size);
|
|
||||||
let n_steps = (args.time.as_secs() as f64 / args.step_size) as usize;
|
|
||||||
|
|
||||||
check_output_file(&args.output_file, args.force_overwrite).unwrap();
|
|
||||||
|
|
||||||
let pb = ProgressBar::new(n_steps.try_into().unwrap());
|
|
||||||
pb.set_style(
|
|
||||||
ProgressStyle::with_template("[{elapsed_precise}] {bar:40.cyan/blue} {percent_precise}% \n\
|
|
||||||
Time remaining: {eta} \n\
|
|
||||||
Current simulation speed: {msg}")
|
|
||||||
.unwrap()
|
|
||||||
.progress_chars("=>-"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut sim = Simulation::new(
|
|
||||||
conf.bodies,
|
|
||||||
norm_time(args.step_size),
|
|
||||||
args.steps_per_save,
|
|
||||||
args.output_file,
|
|
||||||
);
|
|
||||||
let start = Instant::now();
|
|
||||||
sim.run(n_steps, Some(|| {
|
|
||||||
let elapsed = start.elapsed().as_secs() as f64 + 1.0;
|
|
||||||
let speed = Duration::from_secs_f64(pb.position() as f64 * args.step_size / elapsed);
|
|
||||||
pb.set_message(format!("{} /sec", format_duration_single_unit(speed)));
|
|
||||||
pb.inc(args.steps_per_save as u64);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
66
src/body.cpp
Normal file
66
src/body.cpp
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#include "body.hpp"
|
||||||
|
#include "calc.hpp"
|
||||||
|
#include <cmath>
|
||||||
|
#include <boost/multiprecision/cpp_dec_float.hpp>
|
||||||
|
|
||||||
|
Body::Body(const Position& X, const Velocity& V, const Mass& m, const std::string& name)
|
||||||
|
: X(X), V(V), m(m), name(name) {
|
||||||
|
A = Acceleration{Decimal(0), Decimal(0), Decimal(0)};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<Position, Velocity, Mass> Body::save() const {
|
||||||
|
return std::make_tuple(X, V, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
Body Body::load(const std::tuple<Position, Velocity, Mass>& tup) {
|
||||||
|
return Body(std::get<0>(tup), std::get<1>(tup), std::get<2>(tup));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Body::step(Decimal step_size) {
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
X[i] += step_size * V[i];
|
||||||
|
V[i] += step_size * A[i];
|
||||||
|
A[i] = Decimal(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Decimal Body::E() const {
|
||||||
|
return ke() + pe();
|
||||||
|
}
|
||||||
|
|
||||||
|
Decimal Body::pe() const {
|
||||||
|
return -m / dist_from_o();
|
||||||
|
}
|
||||||
|
|
||||||
|
Decimal Body::dist_from_o() const {
|
||||||
|
Decimal sum = Decimal(0);
|
||||||
|
for (const auto& x : X) {
|
||||||
|
sum += x * x;
|
||||||
|
}
|
||||||
|
return std::sqrt(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
Decimal Body::ke() const {
|
||||||
|
return Decimal(0.5) * m * (_speed() * _speed());
|
||||||
|
}
|
||||||
|
|
||||||
|
Decimal Body::_speed() const {
|
||||||
|
Decimal sum = Decimal(0);
|
||||||
|
for (const auto& v : V) {
|
||||||
|
sum += v * v;
|
||||||
|
}
|
||||||
|
return std::sqrt(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Body::speed() const {
|
||||||
|
return format_sig_figs(real_vel(_speed()), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Body::toString() const {
|
||||||
|
std::string pos_str, vel_str;
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
pos_str += format_sig_figs(real_pos(X[i]), 3) + "m ";
|
||||||
|
vel_str += format_sig_figs(real_vel(V[i]), 3) + "m/s ";
|
||||||
|
}
|
||||||
|
return name + ": X = " + pos_str + ", V = " + vel_str;
|
||||||
|
}
|
44
src/body.hpp
Normal file
44
src/body.hpp
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "units.hpp"
|
||||||
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
|
class Body {
|
||||||
|
public:
|
||||||
|
Body(const Position& X, const Velocity& V, const Mass& m, const std::string& name = "");
|
||||||
|
|
||||||
|
// Save and load state
|
||||||
|
std::tuple<Position, Velocity, Mass> save() const;
|
||||||
|
static Body load(const std::tuple<Position, Velocity, Mass>& tup);
|
||||||
|
|
||||||
|
// Physics calculations
|
||||||
|
void step(Decimal step_size);
|
||||||
|
Decimal E() const; // Total energy
|
||||||
|
Decimal pe() const; // Potential energy
|
||||||
|
Decimal ke() const; // Kinetic energy
|
||||||
|
Decimal dist_from_o() const; // Distance from origin
|
||||||
|
std::string speed() const; // Speed as formatted string
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const Position& getPosition() const { return X; }
|
||||||
|
const Velocity& getVelocity() const { return V; }
|
||||||
|
const Acceleration& getAcceleration() const { return A; }
|
||||||
|
const Mass& getMass() const { return m; }
|
||||||
|
const std::string& getName() const { return name; }
|
||||||
|
|
||||||
|
// Setters
|
||||||
|
void setAcceleration(const Acceleration& new_A) { A = new_A; }
|
||||||
|
|
||||||
|
// String representation
|
||||||
|
std::string toString() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Position X;
|
||||||
|
Velocity V;
|
||||||
|
Acceleration A;
|
||||||
|
Mass m;
|
||||||
|
std::string name;
|
||||||
|
|
||||||
|
Decimal _speed() const; // Internal speed calculation
|
||||||
|
};
|
147
src/calc.cpp
Normal file
147
src/calc.cpp
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
#include "calc.hpp"
|
||||||
|
#include <cmath>
|
||||||
|
#include <iostream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <boost/multiprecision/cpp_dec_float.hpp>
|
||||||
|
|
||||||
|
std::vector<std::vector<Decimal>> calculate_distances(const std::vector<Position>& positions) {
|
||||||
|
int N = positions.size();
|
||||||
|
std::vector<std::vector<Decimal>> dists(N, std::vector<Decimal>(N, Decimal(0)));
|
||||||
|
|
||||||
|
for (int i = 0; i < N; ++i) {
|
||||||
|
for (int j = i + 1; j < N; ++j) {
|
||||||
|
Decimal sum = Decimal(0);
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
Decimal diff = positions[i][k] - positions[j][k];
|
||||||
|
sum += diff * diff;
|
||||||
|
}
|
||||||
|
Decimal d = std::sqrt(sum);
|
||||||
|
dists[i][j] = d;
|
||||||
|
dists[j][i] = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dists;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string format_sig_figs(Decimal value, int sig_figs) {
|
||||||
|
if (value == 0) {
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << std::scientific << std::setprecision(sig_figs - 1) << value;
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_progress_bar(int iteration, int total, std::chrono::time_point<std::chrono::steady_clock> start_time, int length, Decimal step_size) {
|
||||||
|
float percent = (float)iteration / total * 100;
|
||||||
|
int filled_length = length * iteration / total;
|
||||||
|
|
||||||
|
std::string bar;
|
||||||
|
bar.reserve(length + 1);
|
||||||
|
bar += '[';
|
||||||
|
bar.append(filled_length, '#');
|
||||||
|
bar.append(length - filled_length, '-');
|
||||||
|
bar += ']';
|
||||||
|
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - start_time).count();
|
||||||
|
double steps_per_second = elapsed > 0 ? real_time(Decimal(iteration) * step_size) / elapsed : 0;
|
||||||
|
|
||||||
|
// Determine appropriate time unit
|
||||||
|
std::string time_unit;
|
||||||
|
if (steps_per_second >= 3600) {
|
||||||
|
time_unit = "hour/s";
|
||||||
|
steps_per_second /= 3600;
|
||||||
|
} else if (steps_per_second >= 60) {
|
||||||
|
time_unit = "min/s";
|
||||||
|
steps_per_second /= 60;
|
||||||
|
} else {
|
||||||
|
time_unit = "s/s";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the current line and move cursor to start
|
||||||
|
std::cout << "\r\033[K";
|
||||||
|
|
||||||
|
// Print the progress bar
|
||||||
|
std::cout << bar << " " << std::fixed << std::setprecision(2)
|
||||||
|
<< percent << "% " << std::setprecision(1) << steps_per_second << " " << time_unit << std::flush;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef NCURSES_ENABLED
|
||||||
|
void plot_points_terminal(const std::vector<Position>& vectors, WINDOW* stdscr,
|
||||||
|
Decimal scale, int grid_width, int grid_height) {
|
||||||
|
if (vectors.empty()) {
|
||||||
|
mvwaddstr(stdscr, 0, 0, "No vectors provided.");
|
||||||
|
wrefresh(stdscr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale and round vectors
|
||||||
|
std::vector<std::pair<int, int>> scaled_vectors;
|
||||||
|
for (const auto& vec : vectors) {
|
||||||
|
scaled_vectors.emplace_back(
|
||||||
|
std::round(vec[0] / scale),
|
||||||
|
std::round(vec[1] / scale)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find bounds
|
||||||
|
int min_x = scaled_vectors[0].first;
|
||||||
|
int max_x = min_x;
|
||||||
|
int min_y = scaled_vectors[0].second;
|
||||||
|
int max_y = min_y;
|
||||||
|
|
||||||
|
for (const auto& vec : scaled_vectors) {
|
||||||
|
min_x = std::min(min_x, vec.first);
|
||||||
|
max_x = std::max(max_x, vec.first);
|
||||||
|
min_y = std::min(min_y, vec.second);
|
||||||
|
max_y = std::max(max_y, vec.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center offsets
|
||||||
|
int center_x = (grid_width / 2) - min_x;
|
||||||
|
int center_y = (grid_height / 2) - min_y;
|
||||||
|
|
||||||
|
// Adjust coordinates
|
||||||
|
std::vector<std::pair<int, int>> adjusted_vectors;
|
||||||
|
for (const auto& vec : scaled_vectors) {
|
||||||
|
adjusted_vectors.emplace_back(
|
||||||
|
vec.first + center_x,
|
||||||
|
vec.second + center_y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get terminal bounds
|
||||||
|
int max_terminal_y, max_terminal_x;
|
||||||
|
getmaxyx(stdscr, max_terminal_y, max_terminal_x);
|
||||||
|
max_x = std::min(grid_width, max_terminal_x - 5);
|
||||||
|
max_y = std::min(grid_height, max_terminal_y - 5);
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
for (int i = grid_height; i >= 0; --i) {
|
||||||
|
std::string row = std::to_string(i - center_y) + " | ";
|
||||||
|
for (int j = 0; j <= grid_width; ++j) {
|
||||||
|
bool has_point = false;
|
||||||
|
for (const auto& vec : adjusted_vectors) {
|
||||||
|
if (vec.first == j && vec.second == i) {
|
||||||
|
has_point = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row += has_point ? "● " : ". ";
|
||||||
|
}
|
||||||
|
mvwaddstr(stdscr, max_y - i, 0, row.substr(0, max_terminal_x - 1).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print X-axis labels
|
||||||
|
std::string x_labels = " ";
|
||||||
|
for (int j = 0; j <= max_x; ++j) {
|
||||||
|
x_labels += std::to_string(j - center_x) + " ";
|
||||||
|
}
|
||||||
|
mvwaddstr(stdscr, max_y + 1, 0, x_labels.substr(0, max_terminal_x - 1).c_str());
|
||||||
|
|
||||||
|
wrefresh(stdscr);
|
||||||
|
}
|
||||||
|
#endif
|
22
src/calc.hpp
Normal file
22
src/calc.hpp
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "units.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
// Calculate distances between all bodies
|
||||||
|
std::vector<std::vector<Decimal>> calculate_distances(const std::vector<Position>& positions);
|
||||||
|
|
||||||
|
// Format a number to a specified number of significant figures
|
||||||
|
std::string format_sig_figs(Decimal value, int sig_figs);
|
||||||
|
|
||||||
|
// Print progress bar
|
||||||
|
void print_progress_bar(int iteration, int total, std::chrono::time_point<std::chrono::steady_clock> start_time, int length, Decimal step_size);
|
||||||
|
|
||||||
|
// Terminal plotting functions (if needed)
|
||||||
|
#ifdef NCURSES_ENABLED
|
||||||
|
#include <ncurses.h>
|
||||||
|
void plot_points_terminal(const std::vector<Position>& vectors, WINDOW* stdscr,
|
||||||
|
Decimal scale = 500000, int grid_width = 30, int grid_height = 30);
|
||||||
|
#endif
|
@ -1,38 +0,0 @@
|
|||||||
use crate::types;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct Body {
|
|
||||||
pub name: String,
|
|
||||||
pub mass: types::Mass,
|
|
||||||
pub position: types::Position,
|
|
||||||
pub velocity: types::Velocity,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub acceleration: types::Acceleration,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct Normalization {
|
|
||||||
pub m_0: f64, // Mass normalization constant
|
|
||||||
pub t_0: f64, // Time normalization constant
|
|
||||||
pub r_0: f64, // Distance normalization constant
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct Config {
|
|
||||||
pub bodies: Vec<Body>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub normalization: Option<Normalization>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
/// Apply normalization settings if present in the config
|
|
||||||
pub fn apply_normalization(&self) {
|
|
||||||
if let Some(ref norm) = self.normalization {
|
|
||||||
types::set_normalization(norm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
pub mod types;
|
|
||||||
pub mod config;
|
|
||||||
pub mod simulation;
|
|
166
src/main.cpp
Normal file
166
src/main.cpp
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <boost/program_options.hpp>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include "simulator.hpp"
|
||||||
|
#include "body.hpp"
|
||||||
|
#include "units.hpp"
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
namespace po = boost::program_options;
|
||||||
|
|
||||||
|
struct SimulationConfig {
|
||||||
|
std::string config_file;
|
||||||
|
std::string output_file;
|
||||||
|
int steps;
|
||||||
|
int steps_per_save;
|
||||||
|
Decimal step_size; // Changed to Decimal for normalized time
|
||||||
|
bool overwrite_output;
|
||||||
|
double simulation_time; // in seconds (real time)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert time string to seconds
|
||||||
|
double parse_time(const std::string& time_str) {
|
||||||
|
std::string value_str = time_str;
|
||||||
|
std::string unit;
|
||||||
|
|
||||||
|
// Extract the unit (last character)
|
||||||
|
if (!time_str.empty()) {
|
||||||
|
unit = time_str.back();
|
||||||
|
value_str = time_str.substr(0, time_str.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
double value = std::stod(value_str);
|
||||||
|
|
||||||
|
// Convert to seconds based on unit
|
||||||
|
switch (unit[0]) {
|
||||||
|
case 's': return value; // seconds
|
||||||
|
case 'm': return value * 60; // minutes
|
||||||
|
case 'h': return value * 3600; // hours
|
||||||
|
case 'd': return value * 86400; // days
|
||||||
|
default: throw std::runtime_error("Invalid time unit. Use s/m/h/d for seconds/minutes/hours/days");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SimulationConfig parse_command_line(int argc, char* argv[]) {
|
||||||
|
SimulationConfig config;
|
||||||
|
double temp_step_size; // Temporary variable for parsing
|
||||||
|
|
||||||
|
po::options_description desc("Orbital Simulator Options");
|
||||||
|
desc.add_options()
|
||||||
|
("help,h", "Show help message")
|
||||||
|
("config,c", po::value<std::string>(&config.config_file)->required(),
|
||||||
|
"Path to planet configuration file (JSON)")
|
||||||
|
("output,o", po::value<std::string>(&config.output_file)->required(),
|
||||||
|
"Path to output file")
|
||||||
|
("time,t", po::value<std::string>()->required(),
|
||||||
|
"Simulation time with unit (e.g., 1h for 1 hour, 30m for 30 minutes, 2d for 2 days)")
|
||||||
|
("step-size,s", po::value<double>(&temp_step_size)->default_value(1.0),
|
||||||
|
"Simulation step size in seconds")
|
||||||
|
("steps-per-save,p", po::value<int>(&config.steps_per_save)->default_value(100),
|
||||||
|
"Number of steps between saves")
|
||||||
|
("overwrite,w", po::bool_switch(&config.overwrite_output),
|
||||||
|
"Overwrite output file if it exists");
|
||||||
|
|
||||||
|
po::variables_map vm;
|
||||||
|
try {
|
||||||
|
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||||
|
|
||||||
|
if (vm.count("help")) {
|
||||||
|
std::cout << desc << "\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
po::notify(vm);
|
||||||
|
|
||||||
|
// Parse simulation time
|
||||||
|
config.simulation_time = parse_time(vm["time"].as<std::string>());
|
||||||
|
|
||||||
|
// Convert step size to Decimal and normalize
|
||||||
|
config.step_size = norm_time(Decimal(temp_step_size));
|
||||||
|
|
||||||
|
// Calculate number of steps based on normalized time and step size
|
||||||
|
config.steps = static_cast<int>(norm_time(config.simulation_time) / config.step_size);
|
||||||
|
|
||||||
|
} catch (const po::error& e) {
|
||||||
|
std::cerr << "Error: " << e.what() << "\n";
|
||||||
|
std::cerr << desc << "\n";
|
||||||
|
exit(1);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Error: " << e.what() << "\n";
|
||||||
|
std::cerr << desc << "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Body> load_planets(const std::string& config_file) {
|
||||||
|
std::ifstream f(config_file);
|
||||||
|
if (!f.is_open()) {
|
||||||
|
throw std::runtime_error("Could not open config file: " + config_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
json j;
|
||||||
|
f >> j;
|
||||||
|
|
||||||
|
std::vector<Body> bodies;
|
||||||
|
for (const auto& planet : j["planets"]) {
|
||||||
|
std::string name = planet["name"];
|
||||||
|
double mass = planet["mass"];
|
||||||
|
|
||||||
|
std::vector<double> pos = planet["position"];
|
||||||
|
std::vector<double> vel = planet["velocity"];
|
||||||
|
|
||||||
|
if (pos.size() != 3 || vel.size() != 3) {
|
||||||
|
throw std::runtime_error("Position and velocity must be 3D vectors");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize units before creating the body
|
||||||
|
Position position{norm_pos(pos[0]), norm_pos(pos[1]), norm_pos(pos[2])};
|
||||||
|
Velocity velocity{norm_vel(vel[0]), norm_vel(vel[1]), norm_vel(vel[2])};
|
||||||
|
Mass normalized_mass = norm_mass(mass);
|
||||||
|
|
||||||
|
bodies.emplace_back(position, velocity, normalized_mass, name);
|
||||||
|
std::cout << "Loaded " << name << " with mass " << mass << " kg\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return bodies;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
try {
|
||||||
|
// Parse command line options
|
||||||
|
auto config = parse_command_line(argc, argv);
|
||||||
|
|
||||||
|
// Load planets from config file
|
||||||
|
auto bodies = load_planets(config.config_file);
|
||||||
|
|
||||||
|
// Create and run simulator
|
||||||
|
Simulator simulator(
|
||||||
|
bodies,
|
||||||
|
config.step_size,
|
||||||
|
config.steps_per_save,
|
||||||
|
config.output_file,
|
||||||
|
0, // current_step
|
||||||
|
config.overwrite_output
|
||||||
|
);
|
||||||
|
|
||||||
|
std::cout << "Starting simulation with " << bodies.size() << " bodies\n";
|
||||||
|
std::cout << "Step size: " << real_time(config.step_size) << " seconds\n";
|
||||||
|
std::cout << "Simulation time: " << config.simulation_time << " seconds\n";
|
||||||
|
std::cout << "Total steps: " << config.steps << "\n";
|
||||||
|
std::cout << "Steps per save: " << config.steps_per_save << "\n";
|
||||||
|
|
||||||
|
simulator.run(config.steps);
|
||||||
|
|
||||||
|
std::cout << "Simulation completed successfully\n";
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Error: " << e.what() << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
@ -1,117 +0,0 @@
|
|||||||
use std::fs::OpenOptions;
|
|
||||||
use std::io::BufWriter;
|
|
||||||
|
|
||||||
use serde::Serialize;
|
|
||||||
use log::{debug, trace};
|
|
||||||
use glam::DVec3;
|
|
||||||
|
|
||||||
use crate::config::Body;
|
|
||||||
use crate::types::{norm_mass, norm_pos, norm_vel, norm_time, real_pos, real_vel, real_time, real_body};
|
|
||||||
|
|
||||||
pub struct Simulation {
|
|
||||||
pub bodies: Vec<Body>,
|
|
||||||
step_size: f64,
|
|
||||||
steps_per_save: usize,
|
|
||||||
save_file: String
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Snapshot<'a> {
|
|
||||||
time: f64,
|
|
||||||
bodies: &'a [Body],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Simulation {
|
|
||||||
pub fn new(
|
|
||||||
mut bodies: Vec<Body>,
|
|
||||||
mut step_size: f64,
|
|
||||||
steps_per_save: usize,
|
|
||||||
save_file: String,
|
|
||||||
) -> Self {
|
|
||||||
let n = bodies.len();
|
|
||||||
for i in 0..n {
|
|
||||||
bodies[i].mass = norm_mass(bodies[i].mass);
|
|
||||||
bodies[i].position = norm_pos(bodies[i].position);
|
|
||||||
bodies[i].velocity = norm_vel(bodies[i].velocity);
|
|
||||||
}
|
|
||||||
step_size = step_size;
|
|
||||||
Self {bodies, step_size, steps_per_save, save_file}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run<F>(&mut self, steps: usize, mut on_step: Option<F>)
|
|
||||||
where F: FnMut() {
|
|
||||||
let file = OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.write(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(&self.save_file);
|
|
||||||
let mut writer = BufWriter::new(file.unwrap());
|
|
||||||
|
|
||||||
for i in 0..steps {
|
|
||||||
trace!("norm step size is {}", self.step_size);
|
|
||||||
trace!("real step size is {}", real_time(self.step_size));
|
|
||||||
trace!("Step: {}, Time: {}", i, real_time(i as f64 * self.step_size));
|
|
||||||
self.reset_accelerations();
|
|
||||||
self.calculate_accelerations();
|
|
||||||
self.step_bodies();
|
|
||||||
if i % self.steps_per_save == 0 {
|
|
||||||
//save the state
|
|
||||||
let real_bodies: Vec<Body> = self.bodies.iter().map(real_body).collect();
|
|
||||||
let snapshot = Snapshot {
|
|
||||||
time: real_time(i as f64 * self.step_size),
|
|
||||||
bodies: &real_bodies,
|
|
||||||
};
|
|
||||||
|
|
||||||
bincode::serialize_into(&mut writer, &snapshot).expect("Couldn't write to trajectory. ");
|
|
||||||
//Do the progress bar
|
|
||||||
if let Some(f) = &mut on_step {
|
|
||||||
f();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_accelerations(&mut self) {
|
|
||||||
let r_hat_over_r3 = self.get_rhat_over_r_three();
|
|
||||||
let n = self.bodies.len();
|
|
||||||
for i in 0..(n-1) {
|
|
||||||
let mass_i = self.bodies[i].mass;
|
|
||||||
for j in (i+1)..n {
|
|
||||||
let mass_j = self.bodies[j].mass;
|
|
||||||
// In normalized units, G = 1
|
|
||||||
self.bodies[i].acceleration += r_hat_over_r3[i][j] * mass_j;
|
|
||||||
self.bodies[j].acceleration -= r_hat_over_r3[i][j] * mass_i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset_accelerations(&mut self) {
|
|
||||||
for i in 0..self.bodies.len() {
|
|
||||||
self.bodies[i].acceleration = DVec3::ZERO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_rhat_over_r_three (&self) -> Vec<Vec<DVec3>>{
|
|
||||||
let n = self.bodies.len();
|
|
||||||
let mut r_hat_over_r3 = vec![vec![DVec3::ZERO; n]; n];
|
|
||||||
for i in 0..(n-1) {
|
|
||||||
for j in (i+1)..n {
|
|
||||||
let r_ij = self.bodies[j].position - self.bodies[i].position;
|
|
||||||
let dist = r_ij.length();
|
|
||||||
r_hat_over_r3[i][j] = r_ij / dist.powf(3.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trace!("rhat over r3: {:?}", r_hat_over_r3);
|
|
||||||
return r_hat_over_r3;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn step_bodies(&mut self) {
|
|
||||||
for body in &mut self.bodies {
|
|
||||||
//do for each of three (x,y,z) dimensions
|
|
||||||
body.position += self.step_size*body.velocity;
|
|
||||||
body.velocity += self.step_size*body.acceleration;
|
|
||||||
trace!("{} now at {:?}", body.name, real_pos(body.position));
|
|
||||||
trace!("{} moving at {:?}", body.name, real_vel(body.velocity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
130
src/simulator.cpp
Normal file
130
src/simulator.cpp
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
#include "simulator.hpp"
|
||||||
|
#include "calc.hpp"
|
||||||
|
#include <fstream>
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
Simulator::Simulator(const std::vector<Body>& bodies,
|
||||||
|
Decimal step_size,
|
||||||
|
int steps_per_save,
|
||||||
|
const std::filesystem::path& output_file,
|
||||||
|
int current_step,
|
||||||
|
bool overwrite_output)
|
||||||
|
: bodies(bodies)
|
||||||
|
, step_size(step_size)
|
||||||
|
, steps_per_save(steps_per_save)
|
||||||
|
, output_file(output_file)
|
||||||
|
, current_step(current_step) {
|
||||||
|
|
||||||
|
if (std::filesystem::exists(output_file) && !overwrite_output) {
|
||||||
|
throw std::runtime_error("File " + output_file.string() + " exists and overwrite flag not given.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::filesystem::exists(output_file) && overwrite_output) {
|
||||||
|
std::cout << "Warning! Overwriting file: " << output_file.string() << std::endl;
|
||||||
|
// Clear the file if we're overwriting
|
||||||
|
std::ofstream clear(output_file, std::ios::trunc);
|
||||||
|
clear.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write initial header with masses
|
||||||
|
out.open(output_file, std::ios::app);
|
||||||
|
if (!out) {
|
||||||
|
throw std::runtime_error("Failed to open output file: " + output_file.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& body : bodies) {
|
||||||
|
out << body.getName() << ": " << body.getMass() << "\n";
|
||||||
|
}
|
||||||
|
out << "\n";
|
||||||
|
|
||||||
|
// Write initial state
|
||||||
|
checkpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
Simulator Simulator::from_checkpoint(const std::filesystem::path& output_file) {
|
||||||
|
// TODO: Implement checkpoint loading
|
||||||
|
// This would require implementing a binary format for saving/loading checkpoints
|
||||||
|
throw std::runtime_error("Checkpoint loading not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Simulator::run(int steps) {
|
||||||
|
auto start_time = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
for (int i = 0; i < steps; ++i) {
|
||||||
|
calculate_forces();
|
||||||
|
move_bodies();
|
||||||
|
if (i % steps_per_save == 0) {
|
||||||
|
checkpoint();
|
||||||
|
print_progress_bar(i + 1, steps, start_time, 50, step_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Simulator::calculate_forces() {
|
||||||
|
std::vector<Position> positions;
|
||||||
|
positions.reserve(bodies.size());
|
||||||
|
for (const auto& body : bodies) {
|
||||||
|
positions.push_back(body.getPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto dists = calculate_distances(positions);
|
||||||
|
|
||||||
|
// Reset all accelerations to zero
|
||||||
|
for (auto& body : bodies) {
|
||||||
|
body.setAcceleration(Acceleration{Decimal(0), Decimal(0), Decimal(0)});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < bodies.size(); i++){
|
||||||
|
for (size_t j = i + 1; j < bodies.size(); j++) {
|
||||||
|
Position vec;
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
vec[k] = bodies[i].getPosition()[k] - bodies[j].getPosition()[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate distance
|
||||||
|
Decimal dist = std::sqrt(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]);
|
||||||
|
|
||||||
|
// Calculate force magnitude using Newton's law of gravitation
|
||||||
|
// F = G * m1 * m2 / r^2 BUT G = 1, and we'll multiply by the opposite mass later
|
||||||
|
// for the acceleration
|
||||||
|
Decimal force_magnitude = 1 / (dist * dist);
|
||||||
|
|
||||||
|
// Calculate acceleration for both bodies
|
||||||
|
Decimal acc_magnitude_i = force_magnitude * bodies[j].getMass();
|
||||||
|
Decimal acc_magnitude_j = force_magnitude * bodies[i].getMass();
|
||||||
|
|
||||||
|
// Convert to vector form
|
||||||
|
Acceleration acc_i, acc_j;
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
acc_i[k] = -vec[k] * acc_magnitude_i / dist;
|
||||||
|
acc_j[k] = vec[k] * acc_magnitude_j / dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to current accelerations
|
||||||
|
Acceleration current_acc_i = bodies[i].getAcceleration();
|
||||||
|
Acceleration current_acc_j = bodies[j].getAcceleration();
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
current_acc_i[k] += acc_i[k];
|
||||||
|
current_acc_j[k] += acc_j[k];
|
||||||
|
}
|
||||||
|
bodies[i].setAcceleration(current_acc_i);
|
||||||
|
bodies[j].setAcceleration(current_acc_j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Simulator::move_bodies() {
|
||||||
|
for (auto& body : bodies) {
|
||||||
|
body.step(step_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Simulator::checkpoint() {
|
||||||
|
for (const auto& body : bodies) {
|
||||||
|
out << body.toString() << "\n";
|
||||||
|
}
|
||||||
|
out << "\n";
|
||||||
|
out.flush();
|
||||||
|
}
|
36
src/simulator.hpp
Normal file
36
src/simulator.hpp
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "body.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
class Simulator {
|
||||||
|
public:
|
||||||
|
Simulator(const std::vector<Body>& bodies,
|
||||||
|
Decimal step_size,
|
||||||
|
int steps_per_save,
|
||||||
|
const std::filesystem::path& output_file,
|
||||||
|
int current_step = 0,
|
||||||
|
bool overwrite_output = false);
|
||||||
|
|
||||||
|
// Create simulator from checkpoint
|
||||||
|
static Simulator from_checkpoint(const std::filesystem::path& output_file);
|
||||||
|
|
||||||
|
// Run simulation
|
||||||
|
void run(int steps);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void calculate_forces();
|
||||||
|
void move_bodies();
|
||||||
|
void checkpoint();
|
||||||
|
|
||||||
|
std::vector<Body> bodies;
|
||||||
|
Decimal step_size;
|
||||||
|
int steps_per_save;
|
||||||
|
std::filesystem::path output_file;
|
||||||
|
std::ofstream out;
|
||||||
|
int current_step;
|
||||||
|
};
|
164
src/types.rs
164
src/types.rs
@ -1,164 +0,0 @@
|
|||||||
#![allow(unused)]
|
|
||||||
use glam::DVec3;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use std::sync::RwLock;
|
|
||||||
use log::{debug};
|
|
||||||
|
|
||||||
use crate::config::{Body, Normalization};
|
|
||||||
|
|
||||||
pub type Position = DVec3;
|
|
||||||
pub type Velocity = DVec3;
|
|
||||||
pub type Acceleration = DVec3;
|
|
||||||
pub type Mass = f64;
|
|
||||||
pub type Time = f64;
|
|
||||||
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
const EARTH_RADIUS: f64 = 6.378e6; //meters
|
|
||||||
const EARTH_MASS: f64 = 5.972e24; //kg
|
|
||||||
const EARTH_ORBITAL_VELOCITY: f64 = 2.9780e4; //m/s
|
|
||||||
const AU: f64 = 1.49597870700e11; //meters
|
|
||||||
|
|
||||||
const MOON_MASS: Mass = 7.34767309e22; // kg
|
|
||||||
const MOON_ORBITAL_VELOCITY: f64 = 1.022e3; //m/s relative to earth
|
|
||||||
|
|
||||||
const SUN_MASS: f64 = 1.989e30; //kg
|
|
||||||
const SUN_RADIUS: f64 = 6.957e8; //meters
|
|
||||||
|
|
||||||
const G: f64 = 6.67430e-11;
|
|
||||||
|
|
||||||
// Default normalization constants
|
|
||||||
const DEFAULT_R_0: f64 = EARTH_RADIUS;
|
|
||||||
const DEFAULT_M_0: f64 = EARTH_MASS;
|
|
||||||
|
|
||||||
// Global normalization context
|
|
||||||
static NORMALIZATION: Lazy<RwLock<NormalizationContext>> = Lazy::new(|| {
|
|
||||||
RwLock::new(NormalizationContext::default())
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct NormalizationContext {
|
|
||||||
pub r_0: f64,
|
|
||||||
pub m_0: f64,
|
|
||||||
pub t_0: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NormalizationContext {
|
|
||||||
fn default() -> Self {
|
|
||||||
let r_0 = DEFAULT_R_0;
|
|
||||||
let m_0 = DEFAULT_M_0;
|
|
||||||
let t_0 = (r_0.powf(3.0) / (G * m_0)).sqrt();
|
|
||||||
debug!("Using default normalization: r_0 = {}, m_0 = {}, t_0 = {}", r_0, m_0, t_0);
|
|
||||||
Self { r_0, m_0, t_0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Normalization> for NormalizationContext {
|
|
||||||
fn from(norm: &Normalization) -> Self {
|
|
||||||
Self {
|
|
||||||
r_0: norm.r_0,
|
|
||||||
m_0: norm.m_0,
|
|
||||||
t_0: norm.t_0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the global normalization context from a config normalization
|
|
||||||
pub fn set_normalization(norm: &Normalization) {
|
|
||||||
let mut context = NORMALIZATION.write().unwrap();
|
|
||||||
*context = NormalizationContext::from(norm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset to default normalization
|
|
||||||
pub fn reset_normalization() {
|
|
||||||
let mut context = NORMALIZATION.write().unwrap();
|
|
||||||
*context = NormalizationContext::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current normalization context
|
|
||||||
pub fn get_normalization() -> NormalizationContext {
|
|
||||||
NORMALIZATION.read().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn norm_pos(pos: Position) -> Position {
|
|
||||||
let norm = get_normalization();
|
|
||||||
pos / norm.r_0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn real_pos(pos: Position) -> Position {
|
|
||||||
let norm = get_normalization();
|
|
||||||
pos * norm.r_0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn norm_mass(mass: Mass) -> Mass {
|
|
||||||
let norm = get_normalization();
|
|
||||||
mass / norm.m_0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn real_mass(mass: Mass) -> Mass {
|
|
||||||
let norm = get_normalization();
|
|
||||||
mass * norm.m_0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn norm_time(time: Time) -> Time {
|
|
||||||
let norm = get_normalization();
|
|
||||||
time / norm.t_0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn real_time(time: Time) -> Time {
|
|
||||||
let norm = get_normalization();
|
|
||||||
time * norm.t_0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn norm_vel(vel: Velocity) -> Velocity {
|
|
||||||
let norm = get_normalization();
|
|
||||||
vel / (norm.r_0 / norm.t_0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn real_vel(vel: Velocity) -> Velocity {
|
|
||||||
let norm = get_normalization();
|
|
||||||
vel * (norm.r_0 / norm.t_0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn norm_acc(acc: Acceleration) -> Acceleration {
|
|
||||||
let norm = get_normalization();
|
|
||||||
acc / (norm.r_0 / (norm.t_0 * norm.t_0))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn real_acc(acc: Acceleration) -> Acceleration {
|
|
||||||
let norm = get_normalization();
|
|
||||||
acc * (norm.r_0 / (norm.t_0 * norm.t_0))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn norm_body(body: Body) -> Body {
|
|
||||||
Body {
|
|
||||||
name: body.name,
|
|
||||||
mass: norm_mass(body.mass),
|
|
||||||
position: norm_pos(body.position),
|
|
||||||
velocity: norm_vel(body.velocity),
|
|
||||||
acceleration: DVec3::ZERO,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn real_body(body: &Body) -> Body {
|
|
||||||
Body {
|
|
||||||
name: body.name.clone(),
|
|
||||||
mass: real_mass(body.mass),
|
|
||||||
position: real_pos(body.position),
|
|
||||||
velocity: real_vel(body.velocity),
|
|
||||||
acceleration: DVec3::ZERO,
|
|
||||||
}
|
|
||||||
}
|
|
44
src/units.hpp
Normal file
44
src/units.hpp
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <cmath>
|
||||||
|
//#include <boost/multiprecision/cpp_dec_float.hpp>
|
||||||
|
|
||||||
|
using Decimal = long double; //boost::multiprecision::cpp_dec_float_50;
|
||||||
|
|
||||||
|
// Type aliases for clarity
|
||||||
|
using Position = std::array<Decimal, 3>;
|
||||||
|
using Velocity = std::array<Decimal, 3>;
|
||||||
|
using Acceleration = std::array<Decimal, 3>;
|
||||||
|
using Mass = Decimal;
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const Decimal EARTH_MASS = 5972e21;//Decimal("5972e21"); // kg
|
||||||
|
const Decimal EARTH_RADIUS = 6378e3;//Decimal("6378e3"); // meters
|
||||||
|
const Decimal EARTH_ORBITAL_VELOCITY = 29780;//Decimal("29780"); // m/s
|
||||||
|
const Decimal AU = 149597870700;//Decimal("149597870700"); // meters
|
||||||
|
|
||||||
|
const Decimal MOON_MASS = 734767309e14;//Decimal("734767309e14");
|
||||||
|
const Decimal MOON_ORBITAL_VELOCITY = 1022;//Decimal("1022"); // m/s relative to earth
|
||||||
|
|
||||||
|
const Decimal SUN_MASS = 1989e27;//Decimal("1989e27"); // kg
|
||||||
|
const Decimal SUN_RADIUS = 6957e5;//Decimal("6957e5"); // meters
|
||||||
|
|
||||||
|
const Decimal PI = 3.14159265358979323846264338327950288419716939937510;
|
||||||
|
|
||||||
|
// Normalizing constants
|
||||||
|
const Decimal G = 6.67430e-11;
|
||||||
|
const Decimal r_0 = EARTH_RADIUS;
|
||||||
|
const Decimal m_0 = 5.972e24;
|
||||||
|
const Decimal t_0 = std::sqrt((r_0 * r_0 * r_0) / (G * m_0));
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
inline Decimal norm_pos(Decimal pos) { return pos / r_0; }
|
||||||
|
inline Decimal real_pos(Decimal pos) { return pos * r_0; }
|
||||||
|
inline Decimal norm_mass(Decimal mass) { return mass / m_0; }
|
||||||
|
inline Decimal real_mass(Decimal mass) { return mass * m_0; }
|
||||||
|
inline Decimal norm_time(Decimal time) { return time / t_0; }
|
||||||
|
inline Decimal real_time(Decimal time) { return time * t_0; }
|
||||||
|
inline Decimal norm_vel(Decimal vel) { return vel / (r_0/t_0); }
|
||||||
|
inline Decimal real_vel(Decimal vel) { return vel * (r_0/t_0); }
|
@ -1,121 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🚀 Starting Orbital Simulator Interfaces..."
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Function to check if a command exists
|
|
||||||
command_exists() {
|
|
||||||
command -v "$1" &> /dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check prerequisites
|
|
||||||
echo -e "${BLUE}Checking prerequisites...${NC}"
|
|
||||||
|
|
||||||
if ! command_exists cargo; then
|
|
||||||
echo -e "${RED}❌ Rust/Cargo not found. Please install Rust first.${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command_exists node; then
|
|
||||||
echo -e "${RED}❌ Node.js not found. Please install Node.js first.${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command_exists python3; then
|
|
||||||
echo -e "${RED}❌ Python 3 not found. Please install Python 3 first.${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Prerequisites check passed${NC}"
|
|
||||||
|
|
||||||
# Build Rust project
|
|
||||||
echo -e "${BLUE}Building Rust project...${NC}"
|
|
||||||
cargo build --release
|
|
||||||
|
|
||||||
# Install web dependencies if needed
|
|
||||||
if [ ! -d "web/node_modules" ]; then
|
|
||||||
echo -e "${BLUE}Installing web dependencies...${NC}"
|
|
||||||
cd web
|
|
||||||
npm install
|
|
||||||
cd ..
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install Python dependencies if needed
|
|
||||||
if [ ! -f ".venv/bin/python" ]; then
|
|
||||||
echo -e "${BLUE}Setting up Python virtual environment...${NC}"
|
|
||||||
python3 -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
else
|
|
||||||
source .venv/bin/activate
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Function to kill background processes on exit
|
|
||||||
cleanup() {
|
|
||||||
echo -e "\n${YELLOW}Shutting down services...${NC}"
|
|
||||||
kill $(jobs -p) 2>/dev/null || true
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
trap cleanup SIGINT SIGTERM
|
|
||||||
|
|
||||||
# Start API server
|
|
||||||
echo -e "${BLUE}Starting API server...${NC}"
|
|
||||||
cargo run --release --bin api_server &
|
|
||||||
API_PID=$!
|
|
||||||
|
|
||||||
# Wait for API server to start
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
# Check if API server is running
|
|
||||||
if curl -s http://localhost:3000/api/configs > /dev/null; then
|
|
||||||
echo -e "${GREEN}✅ API server running on http://localhost:3000${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ API server failed to start${NC}"
|
|
||||||
kill $API_PID 2>/dev/null || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Start web interface
|
|
||||||
echo -e "${BLUE}Starting web interface...${NC}"
|
|
||||||
cd web
|
|
||||||
npm run dev &
|
|
||||||
WEB_PID=$!
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# Wait for web interface to start
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Web interface running on http://localhost:5173${NC}"
|
|
||||||
|
|
||||||
# Start desktop GUI if Tauri is available
|
|
||||||
if command_exists cargo-tauri; then
|
|
||||||
echo -e "${BLUE}Starting desktop GUI...${NC}"
|
|
||||||
cargo tauri dev &
|
|
||||||
GUI_PID=$!
|
|
||||||
echo -e "${GREEN}✅ Desktop GUI launching...${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}⚠️ Tauri not installed. Skipping desktop GUI.${NC}"
|
|
||||||
echo -e "${YELLOW} Install with: cargo install tauri-cli${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "\n${GREEN}🎉 All interfaces started successfully!${NC}"
|
|
||||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
||||||
echo -e "${GREEN}📊 API Server: ${NC}http://localhost:3000"
|
|
||||||
echo -e "${GREEN}🌐 Web Interface: ${NC}http://localhost:5173"
|
|
||||||
if command_exists cargo-tauri; then
|
|
||||||
echo -e "${GREEN}🖥️ Desktop GUI: ${NC}Opening in new window"
|
|
||||||
fi
|
|
||||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
||||||
echo -e "${YELLOW}Press Ctrl+C to stop all services${NC}"
|
|
||||||
|
|
||||||
# Keep script running
|
|
||||||
wait
|
|
44
test.py
Normal file
44
test.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from orbiter.orbits.body import Body
|
||||||
|
from orbiter.orbits.simulator import Simulator
|
||||||
|
from orbiter.units import *
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from decimal import Decimal, getcontext
|
||||||
|
|
||||||
|
getcontext().prec = 50
|
||||||
|
|
||||||
|
#set up the earth
|
||||||
|
earth = Body(
|
||||||
|
Position([0,0,0]),
|
||||||
|
Velocity([0,0,0]),
|
||||||
|
Mass(norm_mass(EARTH_MASS)),
|
||||||
|
"Earth"
|
||||||
|
)
|
||||||
|
|
||||||
|
r = EARTH_RADIUS+100_000
|
||||||
|
#Lets try a body just outside earth accelerating in. Should be 9.8m/s2
|
||||||
|
person = Body(
|
||||||
|
Position([norm_pos(r),0,0]), #10_000m in the sky, airliner height!
|
||||||
|
Velocity([0,(Decimal(0.5)/norm_pos(r)).sqrt(),0]), #orbital velocity
|
||||||
|
Mass(norm_mass(80)), #avg person
|
||||||
|
"Person"
|
||||||
|
)
|
||||||
|
|
||||||
|
T = 2*pi_approx*norm_pos(r)/person.V[1]
|
||||||
|
|
||||||
|
time_to_run = T*3 #norm_time(2000)
|
||||||
|
STEP_SIZE = Decimal(6e-4)
|
||||||
|
n_steps = int(time_to_run/STEP_SIZE)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Before: ")
|
||||||
|
print(str(person))
|
||||||
|
print(str(earth))
|
||||||
|
import cProfile
|
||||||
|
s = Simulator([earth,person], STEP_SIZE, 100, Path("hello_world"))
|
||||||
|
cProfile.run(s.run(n_steps))
|
||||||
|
print("\nAfter:")
|
||||||
|
print(str(person))
|
||||||
|
print(str(earth))
|
||||||
|
|
||||||
|
main()
|
@ -1,200 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Test script for Orbital Simulator Interfaces
|
|
||||||
# This script tests all available interfaces and functionality
|
|
||||||
|
|
||||||
echo "🧪 Testing Orbital Simulator Interfaces"
|
|
||||||
echo "======================================="
|
|
||||||
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
# Test results
|
|
||||||
TESTS_PASSED=0
|
|
||||||
TESTS_FAILED=0
|
|
||||||
|
|
||||||
# Function for test output
|
|
||||||
test_result() {
|
|
||||||
if [ $1 -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}✅ $2${NC}"
|
|
||||||
((TESTS_PASSED++))
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ $2${NC}"
|
|
||||||
((TESTS_FAILED++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 1: Check prerequisites
|
|
||||||
echo -e "${BLUE}Test 1: Prerequisites${NC}"
|
|
||||||
command -v cargo &> /dev/null
|
|
||||||
test_result $? "Rust/Cargo available"
|
|
||||||
|
|
||||||
command -v node &> /dev/null
|
|
||||||
test_result $? "Node.js available"
|
|
||||||
|
|
||||||
command -v npm &> /dev/null
|
|
||||||
test_result $? "npm available"
|
|
||||||
|
|
||||||
command -v python3 &> /dev/null
|
|
||||||
test_result $? "Python 3 available"
|
|
||||||
|
|
||||||
# Test 2: Build Rust project
|
|
||||||
echo -e "\n${BLUE}Test 2: Rust Project Build${NC}"
|
|
||||||
cargo check --bin api_server --no-default-features &> /dev/null
|
|
||||||
test_result $? "API server builds without GUI features"
|
|
||||||
|
|
||||||
cargo check --bin simulator &> /dev/null
|
|
||||||
test_result $? "CLI simulator builds"
|
|
||||||
|
|
||||||
# Test 3: Web dependencies
|
|
||||||
echo -e "\n${BLUE}Test 3: Web Frontend${NC}"
|
|
||||||
cd web
|
|
||||||
npm install &> /dev/null
|
|
||||||
test_result $? "Web dependencies install successfully"
|
|
||||||
|
|
||||||
npm run build &> /dev/null
|
|
||||||
test_result $? "Web frontend builds successfully"
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# Test 4: Start services for testing
|
|
||||||
echo -e "\n${BLUE}Test 4: Service Integration${NC}"
|
|
||||||
|
|
||||||
# Start API server
|
|
||||||
echo "Starting API server for testing..."
|
|
||||||
cargo run --bin api_server --no-default-features &> /dev/null &
|
|
||||||
API_PID=$!
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
# Test API endpoints
|
|
||||||
if curl -s http://localhost:3000/api/configs > /dev/null; then
|
|
||||||
test_result 0 "API server responds to requests"
|
|
||||||
|
|
||||||
# Test creating a simulation
|
|
||||||
RESPONSE=$(curl -s -X POST http://localhost:3000/api/simulations \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"config": {
|
|
||||||
"bodies": [
|
|
||||||
{
|
|
||||||
"name": "Sun",
|
|
||||||
"mass": 1.989e30,
|
|
||||||
"position": [0.0, 0.0, 0.0],
|
|
||||||
"velocity": [0.0, 0.0, 0.0]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Earth",
|
|
||||||
"mass": 5.972e24,
|
|
||||||
"position": [147095000000.0, 0.0, 0.0],
|
|
||||||
"velocity": [0.0, 30290.0, 0.0]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"step_size": 3600,
|
|
||||||
"total_steps": 100
|
|
||||||
}')
|
|
||||||
|
|
||||||
if echo "$RESPONSE" | grep -q '"id"'; then
|
|
||||||
test_result 0 "Simulation creation API works"
|
|
||||||
SIM_ID=$(echo "$RESPONSE" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
|
|
||||||
|
|
||||||
# Test simulation control
|
|
||||||
curl -s -X POST "http://localhost:3000/api/simulations/$SIM_ID/control?action=start" > /dev/null
|
|
||||||
test_result $? "Simulation control API works"
|
|
||||||
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
# Test getting simulation state
|
|
||||||
STATE_RESPONSE=$(curl -s "http://localhost:3000/api/simulations/$SIM_ID")
|
|
||||||
if echo "$STATE_RESPONSE" | grep -q '"step"'; then
|
|
||||||
test_result 0 "Simulation state retrieval works"
|
|
||||||
else
|
|
||||||
test_result 1 "Simulation state retrieval fails"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test simulation list
|
|
||||||
LIST_RESPONSE=$(curl -s http://localhost:3000/api/simulations)
|
|
||||||
if echo "$LIST_RESPONSE" | grep -q "$SIM_ID"; then
|
|
||||||
test_result 0 "Simulation listing works"
|
|
||||||
else
|
|
||||||
test_result 1 "Simulation listing fails"
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
|
||||||
test_result 1 "Simulation creation API fails"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
test_result 1 "API server not responding"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
kill $API_PID 2>/dev/null || true
|
|
||||||
wait $API_PID 2>/dev/null || true
|
|
||||||
|
|
||||||
# Test 5: CLI tools
|
|
||||||
echo -e "\n${BLUE}Test 5: CLI Tools${NC}"
|
|
||||||
|
|
||||||
# Test simulator with sample config
|
|
||||||
if [ -f "config/planets.toml" ]; then
|
|
||||||
timeout 5s cargo run --bin simulator -- --config config/planets.toml --time 1h --step-size 3600 --output-file test_output.bin &> /dev/null
|
|
||||||
test_result $? "CLI simulator runs with config file"
|
|
||||||
rm -f test_output.bin 2>/dev/null
|
|
||||||
else
|
|
||||||
test_result 1 "Sample config file not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 6: Python plotting tools
|
|
||||||
echo -e "\n${BLUE}Test 6: Python Tools${NC}"
|
|
||||||
|
|
||||||
if [ -f "requirements.txt" ]; then
|
|
||||||
# Create virtual environment if it doesn't exist
|
|
||||||
if [ ! -d ".venv" ]; then
|
|
||||||
python3 -m venv .venv
|
|
||||||
fi
|
|
||||||
source .venv/bin/activate
|
|
||||||
pip install -r requirements.txt &> /dev/null
|
|
||||||
test_result $? "Python dependencies install"
|
|
||||||
|
|
||||||
# Test if plotting script can be imported
|
|
||||||
if python3 -c "import plot_trajectories" &> /dev/null; then
|
|
||||||
test_result 0 "Python plotting tools available"
|
|
||||||
else
|
|
||||||
test_result 1 "Python plotting tools not working"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
test_result 1 "Python requirements file not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 7: GUI availability
|
|
||||||
echo -e "\n${BLUE}Test 7: GUI Availability${NC}"
|
|
||||||
|
|
||||||
if command -v cargo-tauri &> /dev/null; then
|
|
||||||
test_result 0 "Tauri CLI available for desktop GUI"
|
|
||||||
|
|
||||||
# Try to check Tauri build (don't actually build to save time)
|
|
||||||
if [ -f "src-tauri/Cargo.toml" ]; then
|
|
||||||
test_result 0 "Tauri project structure exists"
|
|
||||||
else
|
|
||||||
test_result 1 "Tauri project structure missing"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}⚠️ Tauri CLI not installed - desktop GUI not available${NC}"
|
|
||||||
echo " Install with: cargo install tauri-cli"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Final results
|
|
||||||
echo -e "\n${BLUE}Test Results Summary${NC}"
|
|
||||||
echo "==================="
|
|
||||||
echo -e "${GREEN}Tests Passed: $TESTS_PASSED${NC}"
|
|
||||||
echo -e "${RED}Tests Failed: $TESTS_FAILED${NC}"
|
|
||||||
|
|
||||||
if [ $TESTS_FAILED -eq 0 ]; then
|
|
||||||
echo -e "\n${GREEN}🎉 All tests passed! The orbital simulator is ready to use.${NC}"
|
|
||||||
echo -e "${BLUE}Run ./start_interfaces.sh to start all interfaces.${NC}"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo -e "\n${YELLOW}⚠️ Some tests failed. Check the output above for details.${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/orbital-icon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Orbital Simulator</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
4470
web/package-lock.json
generated
4470
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "orbital-simulator-web",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@react-three/drei": "^9.122.0",
|
|
||||||
"@react-three/fiber": "^8.18.0",
|
|
||||||
"@types/uuid": "^9.0.7",
|
|
||||||
"axios": "^1.6.2",
|
|
||||||
"clsx": "^2.0.0",
|
|
||||||
"lucide-react": "^0.294.0",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"recharts": "^2.8.0",
|
|
||||||
"three": "^0.160.1",
|
|
||||||
"uuid": "^9.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^18.2.37",
|
|
||||||
"@types/react-dom": "^18.2.15",
|
|
||||||
"@types/three": "^0.160.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
|
||||||
"@typescript-eslint/parser": "^6.10.0",
|
|
||||||
"@vitejs/plugin-react": "^4.1.1",
|
|
||||||
"eslint": "^8.53.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.4",
|
|
||||||
"typescript": "^5.2.2",
|
|
||||||
"vite": "^5.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
318
web/src/App.tsx
318
web/src/App.tsx
@ -1,318 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import SimulationCanvas from './components/SimulationCanvas';
|
|
||||||
import SimulationControls from './components/SimulationControls';
|
|
||||||
import SimulationList from './components/SimulationList';
|
|
||||||
import ConfigurationPanel from './components/ConfigurationPanel';
|
|
||||||
import { Play, Pause, Square } from 'lucide-react';
|
|
||||||
|
|
||||||
export interface Body {
|
|
||||||
name: string;
|
|
||||||
mass: number;
|
|
||||||
position: [number, number, number];
|
|
||||||
velocity: [number, number, number];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
bodies: Body[];
|
|
||||||
normalization?: {
|
|
||||||
m_0: number;
|
|
||||||
r_0: number;
|
|
||||||
t_0: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SimulationInfo {
|
|
||||||
id: string;
|
|
||||||
is_running: boolean;
|
|
||||||
current_step: number;
|
|
||||||
playback_step: number;
|
|
||||||
total_steps: number;
|
|
||||||
recorded_steps: number;
|
|
||||||
bodies_count: number;
|
|
||||||
elapsed_time: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BodyState {
|
|
||||||
name: string;
|
|
||||||
position: [number, number, number];
|
|
||||||
velocity: [number, number, number];
|
|
||||||
mass: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SimulationUpdate {
|
|
||||||
step: number;
|
|
||||||
time: number;
|
|
||||||
bodies: BodyState[];
|
|
||||||
energy?: {
|
|
||||||
kinetic: number;
|
|
||||||
potential: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [simulations, setSimulations] = useState<SimulationInfo[]>([]);
|
|
||||||
const [selectedSimulation, setSelectedSimulation] = useState<string | null>(null);
|
|
||||||
const [currentData, setCurrentData] = useState<SimulationUpdate | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [isAutoPlaying, setIsAutoPlaying] = useState(true); // Auto-play timeline by default
|
|
||||||
const [sessionId] = useState(() => {
|
|
||||||
// Generate or retrieve session ID from localStorage
|
|
||||||
let id = localStorage.getItem('orbital-sim-session');
|
|
||||||
if (!id) {
|
|
||||||
id = crypto.randomUUID();
|
|
||||||
localStorage.setItem('orbital-sim-session', id);
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper function to add session header to requests
|
|
||||||
const getHeaders = () => ({
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Session-ID': sessionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch simulations list
|
|
||||||
const fetchSimulations = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/simulations', {
|
|
||||||
headers: { 'X-Session-ID': sessionId },
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setSimulations(data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to fetch simulations');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create new simulation
|
|
||||||
const createSimulation = async (config: Config, stepSize: number, totalSteps: number) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/simulations', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getHeaders(),
|
|
||||||
body: JSON.stringify({
|
|
||||||
config,
|
|
||||||
step_size: stepSize,
|
|
||||||
total_steps: totalSteps,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const newSim = await response.json();
|
|
||||||
setSelectedSimulation(newSim.id);
|
|
||||||
await fetchSimulations();
|
|
||||||
} else {
|
|
||||||
setError('Failed to create simulation');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to create simulation');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Control simulation
|
|
||||||
const controlSimulation = async (id: string, action: string) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/simulations/${id}/control?action=${action}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'X-Session-ID': sessionId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
await fetchSimulations();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(`Failed to ${action} simulation`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete simulation
|
|
||||||
const deleteSimulation = async (id: string) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/simulations/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { 'X-Session-ID': sessionId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
if (selectedSimulation === id) {
|
|
||||||
setSelectedSimulation(null);
|
|
||||||
setCurrentData(null);
|
|
||||||
}
|
|
||||||
await fetchSimulations();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to delete simulation');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Seek to specific simulation step
|
|
||||||
const seekToStep = async (id: string, step: number) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/simulations/${id}/seek?step=${step}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'X-Session-ID': sessionId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setCurrentData(data);
|
|
||||||
await fetchSimulations(); // Update playback_step in simulation info
|
|
||||||
} else {
|
|
||||||
setError('Failed to seek simulation');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to seek simulation');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Timeline control functions
|
|
||||||
const handleSeek = (step: number) => {
|
|
||||||
if (selectedSimulation) {
|
|
||||||
seekToStep(selectedSimulation, step);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleAutoPlay = () => {
|
|
||||||
setIsAutoPlaying(!isAutoPlaying);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRestart = () => {
|
|
||||||
if (selectedSimulation) {
|
|
||||||
seekToStep(selectedSimulation, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Poll simulation data (only when auto-playing or simulation is running)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedSimulation) return;
|
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
|
||||||
const selectedSim = simulations.find(s => s.id === selectedSimulation);
|
|
||||||
|
|
||||||
// Only poll if auto-playing is enabled or if simulation is running
|
|
||||||
if (!isAutoPlaying && selectedSim && !selectedSim.is_running) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/simulations/${selectedSimulation}`, {
|
|
||||||
headers: { 'X-Session-ID': sessionId },
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setCurrentData(data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Silently fail for polling
|
|
||||||
}
|
|
||||||
}, 100); // 10 FPS updates
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [selectedSimulation, isAutoPlaying, simulations]);
|
|
||||||
|
|
||||||
// Initial load
|
|
||||||
useEffect(() => {
|
|
||||||
fetchSimulations();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selectedSim = simulations.find(s => s.id === selectedSimulation);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="app">
|
|
||||||
<header className="header">
|
|
||||||
<div className="logo">Orbital Simulator</div>
|
|
||||||
<div className="controls">
|
|
||||||
{selectedSim && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="button"
|
|
||||||
onClick={() => controlSimulation(selectedSim.id, selectedSim.is_running ? 'pause' : 'start')}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{selectedSim.is_running ? <Pause size={16} /> : <Play size={16} />}
|
|
||||||
{selectedSim.is_running ? 'Pause' : 'Start'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="button secondary"
|
|
||||||
onClick={() => controlSimulation(selectedSim.id, 'step')}
|
|
||||||
disabled={isLoading || selectedSim.is_running}
|
|
||||||
>
|
|
||||||
Step
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="button secondary"
|
|
||||||
onClick={() => controlSimulation(selectedSim.id, 'stop')}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<Square size={16} />
|
|
||||||
Stop
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="main-content">
|
|
||||||
<div className="sidebar">
|
|
||||||
<ConfigurationPanel
|
|
||||||
onCreateSimulation={createSimulation}
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SimulationList
|
|
||||||
simulations={simulations}
|
|
||||||
selectedSimulation={selectedSimulation}
|
|
||||||
currentData={currentData}
|
|
||||||
isAutoPlaying={isAutoPlaying}
|
|
||||||
onSelectSimulation={setSelectedSimulation}
|
|
||||||
onDeleteSimulation={deleteSimulation}
|
|
||||||
onControlSimulation={controlSimulation}
|
|
||||||
onSeek={handleSeek}
|
|
||||||
onToggleAutoPlay={handleToggleAutoPlay}
|
|
||||||
onRestart={handleRestart}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedSim && currentData && (
|
|
||||||
<SimulationControls
|
|
||||||
data={currentData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="simulation-view">
|
|
||||||
{error && (
|
|
||||||
<div className="error">
|
|
||||||
{error}
|
|
||||||
<button
|
|
||||||
className="button secondary"
|
|
||||||
onClick={() => setError(null)}
|
|
||||||
style={{ marginLeft: '1rem' }}
|
|
||||||
>
|
|
||||||
Dismiss
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentData ? (
|
|
||||||
<SimulationCanvas data={currentData} />
|
|
||||||
) : selectedSimulation ? (
|
|
||||||
<div className="loading">Loading simulation data...</div>
|
|
||||||
) : (
|
|
||||||
<div className="loading">Select or create a simulation to begin</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
@ -1,288 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Config, Body } from '../App';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onCreateSimulation: (config: Config, stepSize: number, totalSteps: number) => void;
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConfigurationPanel: React.FC<Props> = ({ onCreateSimulation, isLoading }) => {
|
|
||||||
const [selectedConfig, setSelectedConfig] = useState('planets.toml');
|
|
||||||
const [stepSize, setStepSize] = useState(3600);
|
|
||||||
const [simulationTime, setSimulationTime] = useState(365);
|
|
||||||
const [customConfig, setCustomConfig] = useState<Config | null>(null);
|
|
||||||
const [showCustomEditor, setShowCustomEditor] = useState(false);
|
|
||||||
|
|
||||||
// Predefined configurations
|
|
||||||
const presetConfigs: Record<string, Config> = {
|
|
||||||
'earthsun_corrected.toml': {
|
|
||||||
bodies: [
|
|
||||||
{
|
|
||||||
name: 'Sun',
|
|
||||||
mass: 1.989e30,
|
|
||||||
position: [0.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 0.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Earth',
|
|
||||||
mass: 5.972e24,
|
|
||||||
position: [147095000000.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 30290.0, 0.0],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'inner_solar_system.toml': {
|
|
||||||
bodies: [
|
|
||||||
{
|
|
||||||
name: 'Sun',
|
|
||||||
mass: 1.989e30,
|
|
||||||
position: [0.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 0.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Mercury',
|
|
||||||
mass: 3.30104e23,
|
|
||||||
position: [4.6000e10, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 58970.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Venus',
|
|
||||||
mass: 4.8675e24,
|
|
||||||
position: [1.08939e11, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 34780.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Earth',
|
|
||||||
mass: 5.972e24,
|
|
||||||
position: [1.496e11, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 29789.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Moon',
|
|
||||||
mass: 7.34767309e22,
|
|
||||||
position: [1.49984e11, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 30813.0, 0.0],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'solar_system.toml': {
|
|
||||||
bodies: [
|
|
||||||
{
|
|
||||||
name: 'Sun',
|
|
||||||
mass: 1.989e30,
|
|
||||||
position: [0.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 0.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Mercury',
|
|
||||||
mass: 3.30104e23,
|
|
||||||
position: [4.6000e10, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 58970.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Venus',
|
|
||||||
mass: 4.8675e24,
|
|
||||||
position: [1.08939e11, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 34780.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Earth',
|
|
||||||
mass: 5.972e24,
|
|
||||||
position: [1.496e11, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 29789.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Mars',
|
|
||||||
mass: 6.4171e23,
|
|
||||||
position: [2.279e11, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 24007.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Jupiter',
|
|
||||||
mass: 1.8982e27,
|
|
||||||
position: [7.785e11, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 13070.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Saturn',
|
|
||||||
mass: 5.6834e26,
|
|
||||||
position: [1.432e12, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 9680.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Uranus',
|
|
||||||
mass: 8.6810e25,
|
|
||||||
position: [2.867e12, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 6810.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Neptune',
|
|
||||||
mass: 1.0241e26,
|
|
||||||
position: [4.515e12, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 5430.0, 0.0],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'planets.toml': {
|
|
||||||
bodies: [
|
|
||||||
{
|
|
||||||
name: 'Sun',
|
|
||||||
mass: 1.989e30,
|
|
||||||
position: [0.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 0.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Mercury',
|
|
||||||
mass: 3.30104e23,
|
|
||||||
position: [4.6000e10, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 58970.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Venus',
|
|
||||||
mass: 4.867e24,
|
|
||||||
position: [108941000000.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 34780.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Earth',
|
|
||||||
mass: 5.972e24,
|
|
||||||
position: [147095000000.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 30290.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Moon',
|
|
||||||
mass: 7.34767309e22,
|
|
||||||
position: [147458300000, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 31000.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Mars',
|
|
||||||
mass: 6.4171e23,
|
|
||||||
position: [206620000000.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 26500.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Jupiter',
|
|
||||||
mass: 1.8982e27,
|
|
||||||
position: [740520000000.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 13720.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Saturn',
|
|
||||||
mass: 5.6834e26,
|
|
||||||
position: [1352550000000.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 10180.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Uranus',
|
|
||||||
mass: 8.6810e25,
|
|
||||||
position: [2741300000000.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 7110.0, 0.0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Neptune',
|
|
||||||
mass: 1.0241e26,
|
|
||||||
position: [4444450000000.0, 0.0, 0.0],
|
|
||||||
velocity: [0.0, 5500.0, 0.0],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateSimulation = () => {
|
|
||||||
const config = customConfig || presetConfigs[selectedConfig];
|
|
||||||
if (!config) return;
|
|
||||||
|
|
||||||
const totalSteps = Math.floor((simulationTime * 24 * 3600) / stepSize);
|
|
||||||
onCreateSimulation(config, stepSize, totalSteps);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h3>New Simulation</h3>
|
|
||||||
|
|
||||||
<div className="input-group">
|
|
||||||
<label>Configuration</label>
|
|
||||||
<select
|
|
||||||
value={selectedConfig}
|
|
||||||
onChange={(e) => setSelectedConfig(e.target.value)}
|
|
||||||
disabled={showCustomEditor}
|
|
||||||
>
|
|
||||||
<option value="earthsun_corrected.toml">Earth-Sun System</option>
|
|
||||||
<option value="inner_solar_system.toml">Inner Solar System</option>
|
|
||||||
<option value="solar_system.toml">Solar System</option>
|
|
||||||
<option value="planets.toml">Complete System</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group">
|
|
||||||
<label>Step Size (seconds)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={stepSize}
|
|
||||||
onChange={(e) => setStepSize(Number(e.target.value))}
|
|
||||||
min={1}
|
|
||||||
max={86400}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input-group">
|
|
||||||
<label>Simulation Time (days)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={simulationTime}
|
|
||||||
onChange={(e) => setSimulationTime(Number(e.target.value))}
|
|
||||||
min={1}
|
|
||||||
max={10000}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="button"
|
|
||||||
onClick={handleCreateSimulation}
|
|
||||||
disabled={isLoading}
|
|
||||||
style={{ width: '100%', marginBottom: '1rem' }}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Creating...' : 'Create Simulation'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="button secondary"
|
|
||||||
onClick={() => setShowCustomEditor(!showCustomEditor)}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
{showCustomEditor ? 'Use Preset' : 'Custom Config'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showCustomEditor && (
|
|
||||||
<div className="input-group">
|
|
||||||
<label>Custom Configuration (JSON)</label>
|
|
||||||
<textarea
|
|
||||||
rows={10}
|
|
||||||
placeholder={JSON.stringify(presetConfigs['earthsun_corrected.toml'], null, 2)}
|
|
||||||
onChange={(e) => {
|
|
||||||
try {
|
|
||||||
const config = JSON.parse(e.target.value);
|
|
||||||
setCustomConfig(config);
|
|
||||||
} catch {
|
|
||||||
setCustomConfig(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
background: '#333',
|
|
||||||
border: '1px solid #555',
|
|
||||||
borderRadius: '4px',
|
|
||||||
color: 'white',
|
|
||||||
padding: '0.5rem',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '12px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConfigurationPanel;
|
|
@ -1,299 +0,0 @@
|
|||||||
import React, { useRef, useEffect, useState } from 'react';
|
|
||||||
import { Canvas, useFrame } from '@react-three/fiber';
|
|
||||||
import { OrbitControls, Text } from '@react-three/drei';
|
|
||||||
import * as THREE from 'three';
|
|
||||||
|
|
||||||
interface Body {
|
|
||||||
name: string;
|
|
||||||
position: [number, number, number];
|
|
||||||
velocity: [number, number, number];
|
|
||||||
mass: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SimulationUpdate {
|
|
||||||
step: number;
|
|
||||||
time: number;
|
|
||||||
bodies: Body[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: SimulationUpdate;
|
|
||||||
resetTrails?: boolean; // Signal to reset all trails
|
|
||||||
}
|
|
||||||
|
|
||||||
// Component for rendering a celestial body
|
|
||||||
function CelestialBody({ body, index, scaleFactor, resetTrails }: {
|
|
||||||
body: Body;
|
|
||||||
index: number;
|
|
||||||
scaleFactor: number;
|
|
||||||
resetTrails?: boolean;
|
|
||||||
}) {
|
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
|
||||||
const trailRef = useRef<THREE.Line>(null);
|
|
||||||
const trailPoints = useRef<THREE.Vector3[]>([]);
|
|
||||||
const lastPosition = useRef<THREE.Vector3 | null>(null);
|
|
||||||
const lastScaleFactor = useRef<number>(scaleFactor);
|
|
||||||
|
|
||||||
// Color palette for different bodies
|
|
||||||
const colors = [
|
|
||||||
'#FFD700', // Gold for Sun
|
|
||||||
'#FFA500', // Orange for Mercury
|
|
||||||
'#FF6347', // Tomato for Venus
|
|
||||||
'#4169E1', // Royal Blue for Earth
|
|
||||||
'#FF0000', // Red for Mars
|
|
||||||
'#DAA520', // Goldenrod for Jupiter
|
|
||||||
'#F4A460', // Sandy Brown for Saturn
|
|
||||||
'#40E0D0', // Turquoise for Uranus
|
|
||||||
'#0000FF', // Blue for Neptune
|
|
||||||
'#DDA0DD', // Plum for other bodies
|
|
||||||
];
|
|
||||||
|
|
||||||
const color = colors[index % colors.length];
|
|
||||||
|
|
||||||
// Scale positions based on the scale factor
|
|
||||||
// Default scale factor of 1 means 1 AU, scale factor of 0.1 means 0.1 AU, etc.
|
|
||||||
const AU = 1.496e11; // 1 AU in meters
|
|
||||||
const scaledPosition = [
|
|
||||||
body.position[0] / (AU * scaleFactor),
|
|
||||||
body.position[1] / (AU * scaleFactor),
|
|
||||||
body.position[2] / (AU * scaleFactor)
|
|
||||||
];
|
|
||||||
|
|
||||||
// Calculate sphere size with better scaling for visualization
|
|
||||||
// Make the Sun larger and planets visible
|
|
||||||
let size;
|
|
||||||
if (body.name.toLowerCase().includes('sun')) {
|
|
||||||
size = 0.2; // Sun gets a fixed, visible size
|
|
||||||
} else {
|
|
||||||
// Scale planet sizes logarithmically but make them visible
|
|
||||||
const earthMass = 5.972e24;
|
|
||||||
const massRatio = body.mass / earthMass;
|
|
||||||
size = Math.max(0.03, Math.min(Math.pow(massRatio, 1/3) * 0.08, 0.15));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update trail with AU-scaled positions
|
|
||||||
useEffect(() => {
|
|
||||||
const position = new THREE.Vector3(...scaledPosition);
|
|
||||||
|
|
||||||
// Check if we need to reset trails
|
|
||||||
const shouldReset = resetTrails ||
|
|
||||||
Math.abs(scaleFactor - lastScaleFactor.current) > 0.01 || // Scale factor changed
|
|
||||||
(lastPosition.current && position.distanceTo(lastPosition.current) > 5); // Big position jump
|
|
||||||
|
|
||||||
if (shouldReset) {
|
|
||||||
// Reset trails for discontinuous changes
|
|
||||||
trailPoints.current = [position.clone()];
|
|
||||||
} else {
|
|
||||||
// Normal trail update
|
|
||||||
trailPoints.current.push(position.clone());
|
|
||||||
|
|
||||||
// Keep trail length manageable
|
|
||||||
if (trailPoints.current.length > 1000) {
|
|
||||||
trailPoints.current.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update references for next comparison
|
|
||||||
lastPosition.current = position.clone();
|
|
||||||
lastScaleFactor.current = scaleFactor;
|
|
||||||
|
|
||||||
// Update trail geometry
|
|
||||||
if (trailRef.current && trailPoints.current.length > 1) {
|
|
||||||
const geometry = new THREE.BufferGeometry().setFromPoints(trailPoints.current);
|
|
||||||
trailRef.current.geometry.dispose();
|
|
||||||
trailRef.current.geometry = geometry;
|
|
||||||
}
|
|
||||||
}, [body.position, scaleFactor, resetTrails]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
{/* Trail */}
|
|
||||||
{trailPoints.current.length > 1 && (
|
|
||||||
<primitive
|
|
||||||
object={new THREE.Line(
|
|
||||||
new THREE.BufferGeometry().setFromPoints(trailPoints.current),
|
|
||||||
new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.5 })
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Body */}
|
|
||||||
<mesh
|
|
||||||
ref={meshRef}
|
|
||||||
position={scaledPosition as [number, number, number]}
|
|
||||||
>
|
|
||||||
<sphereGeometry args={[size, 16, 16]} />
|
|
||||||
<meshStandardMaterial
|
|
||||||
color={color}
|
|
||||||
emissive={index === 0 ? color : '#000000'} // Sun glows
|
|
||||||
emissiveIntensity={index === 0 ? 0.3 : 0}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
{/* Body label */}
|
|
||||||
<Text
|
|
||||||
position={[
|
|
||||||
scaledPosition[0],
|
|
||||||
scaledPosition[1] + size + 0.3,
|
|
||||||
scaledPosition[2],
|
|
||||||
]}
|
|
||||||
fontSize={0.1}
|
|
||||||
color="white"
|
|
||||||
anchorX="center"
|
|
||||||
anchorY="middle"
|
|
||||||
>
|
|
||||||
{body.name}
|
|
||||||
</Text>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Component for the 3D scene
|
|
||||||
function Scene({ data, scaleFactor, resetTrails }: Props & { scaleFactor: number }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Lighting */}
|
|
||||||
<ambientLight intensity={0.3} />
|
|
||||||
<pointLight position={[0, 0, 0]} intensity={2} color="#FFD700" />
|
|
||||||
<pointLight position={[100, 100, 100]} intensity={0.5} />
|
|
||||||
|
|
||||||
{/* Bodies */}
|
|
||||||
{data.bodies.map((body, index) => (
|
|
||||||
<CelestialBody
|
|
||||||
key={body.name}
|
|
||||||
body={body}
|
|
||||||
index={index}
|
|
||||||
scaleFactor={scaleFactor}
|
|
||||||
resetTrails={resetTrails}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Grid removed for cleaner space view */}
|
|
||||||
|
|
||||||
{/* Controls */}
|
|
||||||
<OrbitControls
|
|
||||||
enablePan={true}
|
|
||||||
enableZoom={true}
|
|
||||||
enableRotate={true}
|
|
||||||
minDistance={0.5}
|
|
||||||
maxDistance={50}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const SimulationCanvas: React.FC<Props> = ({ data, resetTrails }) => {
|
|
||||||
const [scaleFactor, setScaleFactor] = useState(1.0); // 1.0 = 1 AU
|
|
||||||
const [internalResetTrails, setInternalResetTrails] = useState(false);
|
|
||||||
const lastStep = useRef<number>(data.step);
|
|
||||||
const lastScaleFactor = useRef<number>(1.0);
|
|
||||||
|
|
||||||
// Detect big timeline jumps
|
|
||||||
useEffect(() => {
|
|
||||||
const stepDifference = Math.abs(data.step - lastStep.current);
|
|
||||||
const isBigJump = stepDifference > 100; // Consider jumps > 100 steps as "big"
|
|
||||||
|
|
||||||
if (isBigJump) {
|
|
||||||
setInternalResetTrails(true);
|
|
||||||
// Reset the flag after a short delay to trigger the effect
|
|
||||||
setTimeout(() => setInternalResetTrails(false), 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastStep.current = data.step;
|
|
||||||
}, [data.step]);
|
|
||||||
|
|
||||||
// Handle external reset signals and scale changes
|
|
||||||
useEffect(() => {
|
|
||||||
const scaleChanged = Math.abs(scaleFactor - lastScaleFactor.current) > 0.01;
|
|
||||||
|
|
||||||
if (resetTrails || scaleChanged) {
|
|
||||||
setInternalResetTrails(true);
|
|
||||||
setTimeout(() => setInternalResetTrails(false), 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastScaleFactor.current = scaleFactor;
|
|
||||||
}, [resetTrails, scaleFactor]);
|
|
||||||
|
|
||||||
const scaleOptions = [
|
|
||||||
{ value: 0.1, label: '0.1 AU', description: 'Inner planets' },
|
|
||||||
{ value: 0.5, label: '0.5 AU', description: 'Close view' },
|
|
||||||
{ value: 1.0, label: '1 AU', description: 'Default' },
|
|
||||||
{ value: 2.0, label: '2 AU', description: 'Wide view' },
|
|
||||||
{ value: 5.0, label: '5 AU', description: 'Outer system' },
|
|
||||||
{ value: 10.0, label: '10 AU', description: 'Very wide' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const getCurrentScaleLabel = () => {
|
|
||||||
const current = scaleOptions.find(opt => opt.value === scaleFactor);
|
|
||||||
return current ? current.label : `${scaleFactor} AU`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ width: '100%', height: '100%' }}>
|
|
||||||
<Canvas
|
|
||||||
camera={{
|
|
||||||
position: [5, 5, 5],
|
|
||||||
fov: 60,
|
|
||||||
}}
|
|
||||||
style={{ background: '#000' }}
|
|
||||||
>
|
|
||||||
<Scene data={data} scaleFactor={scaleFactor} resetTrails={resetTrails || internalResetTrails} />
|
|
||||||
</Canvas>
|
|
||||||
|
|
||||||
{/* Scale control slider */}
|
|
||||||
<div style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '1rem',
|
|
||||||
right: '1rem',
|
|
||||||
background: 'rgba(0, 0, 0, 0.7)',
|
|
||||||
padding: '1rem',
|
|
||||||
borderRadius: '8px',
|
|
||||||
color: 'white',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
minWidth: '200px',
|
|
||||||
}}>
|
|
||||||
<h4 style={{ margin: '0 0 0.5rem 0', fontSize: '0.9rem' }}>View Scale</h4>
|
|
||||||
<div style={{ marginBottom: '0.5rem' }}>
|
|
||||||
<span style={{ fontSize: '0.8rem', color: '#ccc' }}>Scale: {getCurrentScaleLabel()}</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0.1"
|
|
||||||
max="10"
|
|
||||||
step="0.1"
|
|
||||||
value={scaleFactor}
|
|
||||||
onChange={(e) => setScaleFactor(parseFloat(e.target.value))}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
marginBottom: '0.5rem',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.7rem', color: '#999' }}>
|
|
||||||
<span>Close</span>
|
|
||||||
<span>Far</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Overlay information */}
|
|
||||||
<div style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '1rem',
|
|
||||||
left: '1rem',
|
|
||||||
background: 'rgba(0, 0, 0, 0.7)',
|
|
||||||
padding: '1rem',
|
|
||||||
borderRadius: '8px',
|
|
||||||
color: 'white',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
}}>
|
|
||||||
<div>Step: {data.step.toLocaleString()}</div>
|
|
||||||
<div>Time: {(data.time / 86400).toFixed(2)} days</div>
|
|
||||||
<div>Bodies: {data.bodies.length}</div>
|
|
||||||
<div style={{ fontSize: '0.9rem', marginTop: '0.5rem', color: '#ccc' }}>
|
|
||||||
Scale: 1 unit = {scaleFactor} AU ({(scaleFactor * 150).toFixed(0)}M km)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SimulationCanvas;
|
|
@ -1,59 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { SimulationUpdate } from '../App';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: SimulationUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SimulationControls: React.FC<Props> = ({ data }) => {
|
|
||||||
const formatNumber = (num: number) => {
|
|
||||||
if (num > 1e9) return `${(num / 1e9).toFixed(2)}B`;
|
|
||||||
if (num > 1e6) return `${(num / 1e6).toFixed(2)}M`;
|
|
||||||
if (num > 1e3) return `${(num / 1e3).toFixed(2)}K`;
|
|
||||||
return num.toFixed(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="controls-panel">
|
|
||||||
<div className="view-controls">
|
|
||||||
<h4>Bodies</h4>
|
|
||||||
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
|
||||||
{data.bodies.map((body) => (
|
|
||||||
<div key={body.name} className="stat-item" style={{ marginBottom: '0.25rem' }}>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<span style={{ fontSize: '0.9rem' }}>{body.name}</span>
|
|
||||||
<span style={{ fontSize: '0.8rem', color: '#999' }}>
|
|
||||||
{formatNumber(Math.sqrt(
|
|
||||||
body.position[0] ** 2 + body.position[1] ** 2 + body.position[2] ** 2
|
|
||||||
) / 1.496e11)} AU
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data.energy && (
|
|
||||||
<div className="view-controls">
|
|
||||||
<h4>Energy</h4>
|
|
||||||
<div className="stats-grid">
|
|
||||||
<div className="stat-item">
|
|
||||||
<div className="stat-label">Kinetic</div>
|
|
||||||
<div className="stat-value">{data.energy.kinetic.toExponential(2)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-item">
|
|
||||||
<div className="stat-label">Potential</div>
|
|
||||||
<div className="stat-value">{data.energy.potential.toExponential(2)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-item">
|
|
||||||
<div className="stat-label">Total</div>
|
|
||||||
<div className="stat-value">{data.energy.total.toExponential(2)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SimulationControls;
|
|
@ -1,138 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { SimulationInfo, SimulationUpdate } from '../App';
|
|
||||||
import { Play, Pause, Square, Trash2 } from 'lucide-react';
|
|
||||||
import TimelineSlider from './TimelineSlider';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
simulations: SimulationInfo[];
|
|
||||||
selectedSimulation: string | null;
|
|
||||||
currentData: SimulationUpdate | null;
|
|
||||||
isAutoPlaying: boolean;
|
|
||||||
onSelectSimulation: (id: string) => void;
|
|
||||||
onDeleteSimulation: (id: string) => void;
|
|
||||||
onControlSimulation: (id: string, action: string) => void;
|
|
||||||
onSeek: (step: number) => void;
|
|
||||||
onToggleAutoPlay: () => void;
|
|
||||||
onRestart: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SimulationList: React.FC<Props> = ({
|
|
||||||
simulations,
|
|
||||||
selectedSimulation,
|
|
||||||
currentData,
|
|
||||||
isAutoPlaying,
|
|
||||||
onSelectSimulation,
|
|
||||||
onDeleteSimulation,
|
|
||||||
onControlSimulation,
|
|
||||||
onSeek,
|
|
||||||
onToggleAutoPlay,
|
|
||||||
onRestart,
|
|
||||||
}) => {
|
|
||||||
const formatTime = (seconds: number) => {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
const secs = Math.floor(seconds % 60);
|
|
||||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="simulation-list-container">
|
|
||||||
<h3>Active Simulations</h3>
|
|
||||||
|
|
||||||
{simulations.length === 0 ? (
|
|
||||||
<div className="loading">No simulations running</div>
|
|
||||||
) : (
|
|
||||||
<div className="simulations-scroll">
|
|
||||||
{simulations.map((sim) => (
|
|
||||||
<div key={sim.id} className="simulation-item">
|
|
||||||
<div
|
|
||||||
className={`simulation-card ${selectedSimulation === sim.id ? 'selected' : ''}`}
|
|
||||||
onClick={() => onSelectSimulation(sim.id)}
|
|
||||||
style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
border: selectedSimulation === sim.id ? '2px solid #00aaff' : '1px solid #444',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="simulation-status">
|
|
||||||
<div className={`status-indicator ${sim.is_running ? 'running' : 'paused'}`} />
|
|
||||||
<span>{sim.is_running ? 'Running' : 'Paused'}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stats-grid">
|
|
||||||
<div className="stat-item">
|
|
||||||
<div className="stat-label">Bodies</div>
|
|
||||||
<div className="stat-value">{sim.bodies_count}</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-item">
|
|
||||||
<div className="stat-label">Step</div>
|
|
||||||
<div className="stat-value">{sim.current_step.toLocaleString()}</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-item">
|
|
||||||
<div className="stat-label">Runtime</div>
|
|
||||||
<div className="stat-value">{formatTime(sim.elapsed_time)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-item">
|
|
||||||
<div className="stat-label">ID</div>
|
|
||||||
<div className="stat-value" style={{ fontSize: '0.7rem' }}>
|
|
||||||
{sim.id.substring(0, 8)}...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.5rem' }}>
|
|
||||||
<button
|
|
||||||
className="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onControlSimulation(sim.id, sim.is_running ? 'pause' : 'start');
|
|
||||||
}}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
{sim.is_running ? <Pause size={14} /> : <Play size={14} />}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="button secondary"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onControlSimulation(sim.id, 'stop');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Square size={14} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="button danger"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (confirm('Delete this simulation?')) {
|
|
||||||
onDeleteSimulation(sim.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timeline slider slides out from selected simulation */}
|
|
||||||
{selectedSimulation === sim.id && currentData && (
|
|
||||||
<div className="timeline-slideout">
|
|
||||||
<TimelineSlider
|
|
||||||
simulation={sim}
|
|
||||||
isAutoPlaying={isAutoPlaying}
|
|
||||||
onSeek={onSeek}
|
|
||||||
onToggleAutoPlay={onToggleAutoPlay}
|
|
||||||
onRestart={onRestart}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SimulationList;
|
|
@ -1,144 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Play, Pause, RotateCcw } from 'lucide-react';
|
|
||||||
import { SimulationInfo } from '../App';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
simulation: SimulationInfo;
|
|
||||||
isAutoPlaying: boolean;
|
|
||||||
onSeek: (step: number) => void;
|
|
||||||
onToggleAutoPlay: () => void;
|
|
||||||
onRestart: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TimelineSlider: React.FC<Props> = ({
|
|
||||||
simulation,
|
|
||||||
isAutoPlaying,
|
|
||||||
onSeek,
|
|
||||||
onToggleAutoPlay,
|
|
||||||
onRestart
|
|
||||||
}) => {
|
|
||||||
const [localStep, setLocalStep] = useState(simulation.playback_step);
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
|
|
||||||
// Update local step when simulation updates (but not while dragging)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isDragging) {
|
|
||||||
setLocalStep(simulation.playback_step);
|
|
||||||
}
|
|
||||||
}, [simulation.playback_step, isDragging]);
|
|
||||||
|
|
||||||
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const step = parseInt(e.target.value);
|
|
||||||
setLocalStep(step);
|
|
||||||
// Immediately seek to the new step for real-time updates
|
|
||||||
onSeek(step);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSliderMouseDown = () => {
|
|
||||||
setIsDragging(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSliderMouseUp = () => {
|
|
||||||
setIsDragging(false);
|
|
||||||
// Final seek to ensure we're at the exact position
|
|
||||||
onSeek(localStep);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSliderKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
onSeek(localStep);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatStep = (step: number) => {
|
|
||||||
if (step > 1e6) return `${(step / 1e6).toFixed(1)}M`;
|
|
||||||
if (step > 1e3) return `${(step / 1e3).toFixed(1)}K`;
|
|
||||||
return step.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const progressPercent = simulation.recorded_steps > 0
|
|
||||||
? (simulation.current_step / Math.max(simulation.recorded_steps, simulation.total_steps)) * 100
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="timeline-slider">
|
|
||||||
<div className="timeline-header">
|
|
||||||
<h4>Timeline Control</h4>
|
|
||||||
<div className="timeline-info">
|
|
||||||
<span>Step: {formatStep(localStep)} / {formatStep(simulation.recorded_steps)}</span>
|
|
||||||
<span>Progress: {progressPercent.toFixed(1)}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="timeline-controls">
|
|
||||||
<button
|
|
||||||
onClick={onRestart}
|
|
||||||
className="control-button"
|
|
||||||
title="Restart from beginning"
|
|
||||||
>
|
|
||||||
<RotateCcw size={16} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={onToggleAutoPlay}
|
|
||||||
className={`control-button ${isAutoPlaying ? 'active' : ''}`}
|
|
||||||
title={isAutoPlaying ? 'Pause auto-play' : 'Start auto-play'}
|
|
||||||
>
|
|
||||||
{isAutoPlaying ? <Pause size={16} /> : <Play size={16} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="slider-container">
|
|
||||||
{/* Progress bar showing simulation progress */}
|
|
||||||
<div className="progress-track">
|
|
||||||
<div
|
|
||||||
className="progress-bar"
|
|
||||||
style={{ width: `${progressPercent}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main timeline slider */}
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max={Math.max(simulation.recorded_steps - 1, 0)}
|
|
||||||
value={localStep}
|
|
||||||
onChange={handleSliderChange}
|
|
||||||
onMouseDown={handleSliderMouseDown}
|
|
||||||
onMouseUp={handleSliderMouseUp}
|
|
||||||
onKeyUp={handleSliderKeyUp}
|
|
||||||
className="timeline-range"
|
|
||||||
disabled={simulation.recorded_steps === 0}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Step markers */}
|
|
||||||
<div className="step-markers">
|
|
||||||
<span className="step-marker start">0</span>
|
|
||||||
<span className="step-marker current">
|
|
||||||
{formatStep(localStep)}
|
|
||||||
</span>
|
|
||||||
<span className="step-marker end">
|
|
||||||
{formatStep(Math.max(simulation.total_steps, simulation.recorded_steps))}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="timeline-status">
|
|
||||||
<div className="status-item">
|
|
||||||
<span className="status-label">Simulation:</span>
|
|
||||||
<span className={`status-value ${simulation.is_running ? 'running' : 'paused'}`}>
|
|
||||||
{simulation.is_running ? 'Running' : 'Paused'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="status-item">
|
|
||||||
<span className="status-label">Auto-play:</span>
|
|
||||||
<span className={`status-value ${isAutoPlaying ? 'active' : 'inactive'}`}>
|
|
||||||
{isAutoPlaying ? 'On' : 'Off'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TimelineSlider;
|
|
@ -1,482 +0,0 @@
|
|||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
||||||
background: #0a0a0a;
|
|
||||||
color: #ffffff;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background: #1a1a1a;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #00aaff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 300px;
|
|
||||||
background: #1a1a1a;
|
|
||||||
border-right: 1px solid #333;
|
|
||||||
padding: 1rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simulation-view {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
background: #00aaff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
background: #0088cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:disabled {
|
|
||||||
background: #666;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.secondary {
|
|
||||||
background: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.secondary:hover {
|
|
||||||
background: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.danger {
|
|
||||||
background: #ff4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.danger:hover {
|
|
||||||
background: #cc0000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group input,
|
|
||||||
.input-group select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem;
|
|
||||||
background: #333;
|
|
||||||
border: 1px solid #555;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simulation-card {
|
|
||||||
background: #2a2a2a;
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simulation-card.selected {
|
|
||||||
transform: translateX(-4px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 170, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.simulation-card h3 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #00aaff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simulation-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.running {
|
|
||||||
background: #00ff00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.paused {
|
|
||||||
background: #ffaa00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
background: #333;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
color: #999;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-panel {
|
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
background: rgba(26, 26, 26, 0.9);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-controls {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-controls h4 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #00aaff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #ff4444;
|
|
||||||
background: rgba(255, 68, 68, 0.1);
|
|
||||||
border: 1px solid #ff4444;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Timeline Slider Styles */
|
|
||||||
.timeline-slider {
|
|
||||||
background: #2a2a2a;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-header h4 {
|
|
||||||
color: #00aaff;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-info {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-button {
|
|
||||||
background: #333;
|
|
||||||
border: 1px solid #555;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #fff;
|
|
||||||
padding: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-button:hover {
|
|
||||||
background: #444;
|
|
||||||
border-color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-button.active {
|
|
||||||
background: #00aaff;
|
|
||||||
border-color: #00aaff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-button:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-container {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-track {
|
|
||||||
height: 4px;
|
|
||||||
background: #333;
|
|
||||||
border-radius: 2px;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, #00aaff, #0088cc);
|
|
||||||
border-radius: 2px;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-range {
|
|
||||||
width: 100%;
|
|
||||||
height: 8px;
|
|
||||||
background: #333;
|
|
||||||
border-radius: 4px;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-range::-webkit-slider-thumb {
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background: #00aaff;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid #fff;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-range::-moz-range-thumb {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background: #00aaff;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid #fff;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-range:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-markers {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-marker {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-marker.current {
|
|
||||||
color: #00aaff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-status {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-label {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-value {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-value.running {
|
|
||||||
color: #00ff88;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-value.paused {
|
|
||||||
color: #ffaa00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-value.active {
|
|
||||||
color: #00aaff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-value.inactive {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Simulation List Enhancements */
|
|
||||||
.simulation-list-container {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simulations-scroll {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
margin-right: -0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simulations-scroll::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simulations-scroll::-webkit-scrollbar-track {
|
|
||||||
background: #1a1a1a;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simulations-scroll::-webkit-scrollbar-thumb {
|
|
||||||
background: #444;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simulations-scroll::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simulation-item {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Timeline Slideout Animation */
|
|
||||||
.timeline-slideout {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
border-left: 2px solid #00aaff;
|
|
||||||
background: rgba(0, 170, 255, 0.05);
|
|
||||||
border-radius: 0 8px 8px 0;
|
|
||||||
animation: slideIn 0.3s ease-out;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
max-height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
max-height: 300px;
|
|
||||||
opacity: 1;
|
|
||||||
padding-top: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-slideout .timeline-slider {
|
|
||||||
margin-bottom: 0;
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-slideout .timeline-header h4 {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-slideout .timeline-controls {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-slideout .control-button {
|
|
||||||
padding: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-slideout .slider-container {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-slideout .timeline-status {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom/client'
|
|
||||||
import App from './App'
|
|
||||||
import './index.css'
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
)
|
|
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noFallthroughCasesInSwitch": true
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
server: {
|
|
||||||
port: 5173,
|
|
||||||
proxy: {
|
|
||||||
'/api': {
|
|
||||||
target: 'http://localhost:4395',
|
|
||||||
changeOrigin: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
outDir: 'dist',
|
|
||||||
}
|
|
||||||
})
|
|
Loading…
x
Reference in New Issue
Block a user