diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b217388 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '[BUG] ' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**System Information:** + - OS: [e.g. Windows 10, Ubuntu 20.04, macOS 12.0] + - Python Version: [e.g. 3.9.7] + - Pygame Version: [e.g. 2.6.1] + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ee606e2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '[FEATURE] ' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. + +**Implementation ideas** +If you have ideas about how this could be implemented, please share them here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/hacktoberfest.md b/.github/ISSUE_TEMPLATE/hacktoberfest.md new file mode 100644 index 0000000..e071bb9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/hacktoberfest.md @@ -0,0 +1,40 @@ +--- +name: Hacktoberfest Issue +about: Issues specifically for Hacktoberfest contributors +title: '[HACKTOBERFEST] ' +labels: hacktoberfest, good first issue +assignees: '' + +--- + +**Issue Description** +A clear description of what needs to be implemented or fixed. + +**Difficulty Level** +- [ ] Beginner (Good first issue) +- [ ] Intermediate +- [ ] Advanced + +**Tasks to Complete** +- [ ] Task 1 +- [ ] Task 2 +- [ ] Task 3 +- [ ] Write tests +- [ ] Update documentation + +**Expected Outcome** +Describe what the end result should look like. + +**Files to Modify** +List the files that will likely need changes: +- `src/...` +- `tests/...` + +**Getting Started** +1. Comment on this issue to claim it +2. Fork the repository +3. Create a feature branch +4. Read the [CONTRIBUTING.md](../../CONTRIBUTING.md) guide + +**Additional Resources** +Links to relevant documentation or examples. \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..f0782fd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,32 @@ +## Description +Brief description of changes made. + +## Type of Change +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Performance improvement +- [ ] Code refactoring + +## Related Issue +Fixes #(issue number) + +## Testing +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] I have tested the changes manually + +## Screenshots (if applicable) +Add screenshots to help explain your changes. + +## Checklist +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] Any dependent changes have been merged and published + +## Additional Notes +Any additional information or context about the changes. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b5832ea --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: [3.8, 3.9, '3.10', 3.11] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install system dependencies (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y python3-dev libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-cov flake8 + + - name: Lint with flake8 + run: | + flake8 src --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 src --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Test with pytest + run: | + pytest tests/ --cov=src --cov-report=xml + + - name: Upload coverage to Codecov + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4a6127b..f082d92 100644 --- a/.gitignore +++ b/.gitignore @@ -1,147 +1,150 @@ -##### Windows -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ +# C extensions +*.so -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db -# Windows shortcuts -*.lnk +# Project specific +screenshots/ +config.json +.misc/ -##### MacOS -# General -.DS_Store -.AppleDouble -.LSOverride - -##### Vim -# Swap -[._]*.s[a-v][a-z] -!*.svg # comment out if you don't need vector files -[._]*.sw[a-p] -[._]s[a-rt-v][a-z] -[._]ss[a-gi-z] -[._]sw[a-p] - -##### VisualStudioCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# CMake -cmake-build-*/ - -build - -##### CMake -CMakeLists.txt.user -CMakeCache.txt -CMakeFiles -CMakeScripts -Testing -Makefile -cmake_install.cmake -install_manifest.txt -compile_commands.json -CTestTestfile.cmake -_deps - -##### C++ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - -# C/C++ binary extension file -*.bin - -##### C -# Prerequisites -*.d - -# Object files -*.o -*.ko -*.obj -*.elf - -# Linker output -*.ilk -*.map -*.exp - -# Precompiled Headers -*.gch -*.pch - -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -*.idb -*.pdb - -# Vcpkg -vcpkg_installed +# Simulation outputs +*.png +*.jpg +*.gif +!simulation.png \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..cfffc81 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,35 @@ +# Code of Conduct + +## Our Pledge + +We pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8ff90bd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,77 @@ +# Contributing to Black Hole Simulation + +Thank you for your interest in contributing to the Black Hole Simulation project! We welcome contributions from developers of all skill levels. + +## How to Contribute + +### Reporting Issues + +- Use the GitHub issue tracker to report bugs +- Include detailed steps to reproduce the issue +- Provide system information (OS, Python version, etc.) +- Include screenshots or videos if applicable + +### Suggesting Features + +- Open an issue with the "enhancement" label +- Describe the feature and its use case +- Explain how it would benefit users + +### Code Contributions + +1. **Fork the repository** +2. **Create a feature branch**: `git checkout -b feature/your-feature-name` +3. **Make your changes** +4. **Write tests** for new functionality +5. **Run the test suite**: `python -m pytest tests/` +6. **Update documentation** if needed +7. **Commit your changes**: `git commit -m "Add feature: description"` +8. **Push to your fork**: `git push origin feature/your-feature-name` +9. **Submit a pull request** + +## Development Guidelines + +### Code Style + +- Follow PEP 8 Python style guidelines +- Use meaningful variable and function names +- Keep functions focused and small +- Add docstrings for public functions and classes + +### Testing + +- Write unit tests for new features +- Ensure all tests pass before submitting PR +- Aim for good test coverage + +### Documentation + +- Update README.md for new features +- Add inline comments for complex logic +- Update docstrings when changing function signatures + +## Areas for Contribution + +### Good First Issues + +- Bug fixes in physics calculations +- UI improvements +- Performance optimizations +- Documentation improvements + +### Advanced Features + +- Relativistic effects implementation +- Better rendering algorithms +- Multi-body gravitational systems +- Save/load simulation states + +## Getting Help + +- Join our discussions in GitHub Issues +- Ask questions in pull request comments +- Check existing issues and PRs before starting work + +## Recognition + +Contributors will be acknowledged in the README and release notes. Thank you for helping make this project better! \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2103439 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Black Hole Simulation Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4cd8abb --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Black Hole Simulation + +A real-time 3D black hole simulation built with Python and Pygame, featuring gravitational physics, particle systems, and visual effects. + +## Features + +- **Realistic Physics**: Implements gravitational acceleration and Schwarzschild radius calculations +- **Particle System**: Simulates accretion disk with hundreds of particles +- **Visual Effects**: Glow effects, particle trails, and gravitational lensing visualization +- **Interactive Camera**: Full 3D camera controls with zoom and movement +- **Real-time Simulation**: 60 FPS simulation with dynamic particle generation + +## Installation + +1. Clone the repository: +```bash +git clone https://github.com/yourusername/black-hole-simulation.git +cd black-hole-simulation +``` + +2. Create a virtual environment: +```bash +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate +``` + +3. Install dependencies: +```bash +pip install -r requirements.txt +``` + +## Usage + +Run the simulation: +```bash +python src/main.py +``` + +### Controls + +- **WASD**: Move camera +- **Q/E**: Move camera up/down +- **+/-**: Zoom in/out +- **R**: Reset camera position +- **G**: Toggle gravity on/off +- **ESC**: Exit simulation + +## Project Structure + +``` +src/ +├── main.py # Main simulation loop +├── physics/ # Physics calculations +│ ├── black_hole.py # Black hole physics +│ ├── particle.py # Particle system +│ └── constants.py # Physical constants +├── rendering/ # Rendering system +│ ├── renderer.py # Main renderer +│ ├── camera.py # Camera controls +│ └── camera_config.py # Camera settings +└── utils/ # Utility functions + └── vectors.py # Vector operations +``` + +## Physics + +The simulation implements: +- **Gravitational Force**: F = GMm/r² +- **Schwarzschild Radius**: Rs = 2GM/c² +- **Event Horizon**: Particles crossing the event horizon turn black +- **Orbital Mechanics**: Particles follow realistic orbital paths + +## Contributing + +We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. + +### Development Setup + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature-name` +3. Make your changes +4. Run tests: `python -m pytest tests/` +5. Submit a pull request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +- Physics calculations based on Einstein's General Relativity +- Inspired by real black hole observations from Event Horizon Telescope +- Built with Python, Pygame, and NumPy \ No newline at end of file diff --git a/check.py b/check.py deleted file mode 100644 index e0cdf65..0000000 --- a/check.py +++ /dev/null @@ -1,20 +0,0 @@ -# # check.py -# import pygame -# from pygame.locals import * # This imports DOUBLEBUF, OPENGL, and other constants -# from OpenGL.GL import * -# from OpenGL.GLU import * -# import numpy as np - -# print("All imports successful!") -# print(f"Pygame version: {pygame.version.ver}") -# print(f"NumPy version: {np.__version__}") - -# # Try to get OpenGL information -# pygame.init() -# display = (800, 600) -# pygame.display.set_mode(display, DOUBLEBUF | OPENGL) - -# print(f"OpenGL version: {glGetString(GL_VERSION).decode()}") -# print(f"GPU: {glGetString(GL_RENDERER).decode()}") - -# pygame.quit() \ No newline at end of file diff --git a/check_with_matplotlib.py b/check_with_matplotlib.py new file mode 100644 index 0000000..17e71f8 --- /dev/null +++ b/check_with_matplotlib.py @@ -0,0 +1,60 @@ +import sys +sys.path.insert(0, 'src') +import matplotlib.pyplot as plt +import numpy as np +from physics.black_hole import BlackHole +from physics.particle import ParticleSystem +from physics.constants import G + +# Create black hole +black_hole = BlackHole(mass=4e37, position=(0, 0, 0)) + +# Create particle system +particle_system = ParticleSystem() + +# Add some particles for testing +num_particles = 10 +inner_radius = black_hole.schwarz_radius * 2.5 +outer_radius = black_hole.schwarz_radius * 12 + +for i in range(num_particles): + distance = np.random.uniform(inner_radius, outer_radius) + angle = np.random.uniform(0, 2 * np.pi) + x = distance * np.cos(angle) + z = distance * np.sin(angle) + y = np.random.uniform(-outer_radius/50, outer_radius/50) + + orbital_speed = np.sqrt(G * black_hole.mass / distance) + vx = -orbital_speed * np.sin(angle) + vz = orbital_speed * np.cos(angle) + + particle_system.add_particle( + mass=np.random.uniform(1e9, 1e10), + position=(x, y, z), + velocity=(vx, 0, vz), + colour=(1, 0, 0) + ) + +# Update a few times +for _ in range(10): + particle_system.update(black_hole, 50.0) + +# Plot +fig = plt.figure() +ax = fig.add_subplot(111, projection='3d') + +# Plot black hole +ax.scatter(0, 0, 0, color='black', s=100, label='Black Hole') + +# Plot particles +for particle in particle_system.particles: + ax.scatter(particle.position[0], particle.position[1], particle.position[2], + color=particle.colour, s=10) + +ax.set_xlabel('X') +ax.set_ylabel('Y') +ax.set_zlabel('Z') +ax.legend() +plt.title('Black Hole Simulation Positions') +plt.savefig('simulation.png') +print("Plot saved to simulation.png") \ No newline at end of file diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..6ec40f9 --- /dev/null +++ b/config.example.json @@ -0,0 +1,31 @@ +{ + "simulation": { + "width": 1200, + "height": 800, + "fps": 60, + "time_step": 50.0 + }, + "black_hole": { + "mass": 4e37, + "position": [0, 0, 0] + }, + "particles": { + "count": 300, + "jet_particles": 30, + "inner_radius_multiplier": 2.5, + "outer_radius_multiplier": 12, + "max_trail_length": 50 + }, + "camera": { + "initial_position": [0, 2e11, -3e11], + "move_speed": 5e13, + "zoom_speed": 0.5, + "initial_zoom": 1.0 + }, + "rendering": { + "glow_effects": true, + "particle_trails": true, + "show_ui": true, + "disk_texture_size": 256 + } +} \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..92d2976 --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +from setuptools import setup, find_packages + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +with open("requirements.txt", "r", encoding="utf-8") as fh: + requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")] + +setup( + name="black-hole-simulation", + version="1.0.0", + author="Black Hole Simulation Contributors", + description="A real-time 3D black hole simulation with gravitational physics", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/yourusername/black-hole-simulation", + packages=find_packages(where="src"), + package_dir={"": "src"}, + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Scientific/Engineering :: Physics", + "Topic :: Scientific/Engineering :: Visualization", + ], + python_requires=">=3.8", + install_requires=requirements, + entry_points={ + "console_scripts": [ + "black-hole-sim=main:main", + ], + }, + keywords="physics simulation black-hole pygame visualization", + project_urls={ + "Bug Reports": "https://github.com/yourusername/black-hole-simulation/issues", + "Source": "https://github.com/yourusername/black-hole-simulation", + "Documentation": "https://github.com/yourusername/black-hole-simulation#readme", + }, +) \ No newline at end of file diff --git a/simulation.md b/simulation.md new file mode 100644 index 0000000..9575fab --- /dev/null +++ b/simulation.md @@ -0,0 +1,52 @@ +# Simulation Documentation + +## Overview +This file documents the main simulation script and utility check script. + +## main.py + +The main entry point for the black hole simulation application. Initializes Pygame, sets up the black hole, particle system, camera, and renderer, then runs the simulation loop. + +### Key Components + +- **Initialization**: + - Initializes Pygame and sets up a window (1200x800). + - Creates a BlackHole with mass 4e37 kg (much larger than default for dramatic effects). + - Initializes ParticleSystem and Camera with custom position. + - Creates Renderer for drawing. + +- **Particle Setup**: + - Generates 300 particles for accretion disk between 2.5 and 12 times Schwarzschild radius. + - Calculates orbital velocities using Keplerian formula \( v = \sqrt{\frac{GM}{r}} \). + - Adds color variation based on distance (redshift simulation). + - Adds 30 high-velocity particles for jet effects. + +- **Main Loop**: + - Handles events (quit, escape, gravity toggle, camera reset). + - Processes continuous camera movement (WASDQE keys). + - Updates particle system with physics time step (50 seconds per frame for stability). + - Periodically adds new particles to maintain disk. + - Renders scene and flips display at 60 FPS. + +- **Physics Integration**: + - Uses Euler method for particle updates. + - Applies gravitational acceleration from black hole. + - Changes particle color to black inside horizon. + +- **Error Handling**: + - Catches rendering errors and falls back to simple circle. + +This script ties together physics, rendering, and user input for an interactive simulation. + +## check.py + +A utility script for verifying dependencies and OpenGL capabilities. Currently commented out, but can be used to check if Pygame, NumPy, and OpenGL are properly installed and functioning. + +### Functionality + +- Imports necessary libraries (Pygame, OpenGL, NumPy). +- Prints versions of Pygame and NumPy. +- Initializes Pygame and creates an OpenGL context. +- Retrieves and prints OpenGL version and GPU information. + +Useful for debugging environment setup before running the main simulation. \ No newline at end of file diff --git a/simulation.png b/simulation.png new file mode 100644 index 0000000..0f355d8 Binary files /dev/null and b/simulation.png differ diff --git a/src/main.py b/src/main.py index 5c350d6..8e71008 100644 --- a/src/main.py +++ b/src/main.py @@ -6,50 +6,48 @@ from physics.particle import ParticleSystem from rendering.renderer import Renderer from rendering.camera import Camera -from physics.constants import G, C +from physics.constants import G def main(): + print("Initializing pygame...") pygame.init() + print("Pygame initialized.") width, height = 1200, 800 screen = pygame.display.set_mode((width, height)) pygame.display.set_caption("Black Hole Simulation") + print("Window created.") - # Create black hole with larger mass for more dramatic effects black_hole = BlackHole(mass=4e37, position=(0, 0, 0)) + print("Black hole created.") particle_system = ParticleSystem() + print("Particle system created.") - # Create camera with better initial position camera = Camera(position=(0, 2e11, -3e11)) renderer = Renderer(width, height, screen) + print("Camera and renderer created.") print(f"Black hole mass: {black_hole.mass:.2e} kg") print(f"Schwarzschild radius: {black_hole.schwarz_radius:.2e} m") - # Create accretion disk with varying properties num_particles = 300 inner_radius = black_hole.schwarz_radius * 2.5 outer_radius = black_hole.schwarz_radius * 12 - for i in range(num_particles): - # Vary distance from black hole + for _ in range(num_particles): distance = np.random.uniform(inner_radius, outer_radius) angle = np.random.uniform(0, 2 * np.pi) - # Position in the accretion disk plane x = distance * np.cos(angle) z = distance * np.sin(angle) - y = np.random.uniform(-outer_radius/50, outer_radius/50) # Slight thickness + y = np.random.uniform(-outer_radius/50, outer_radius/50) - # Calculate orbital velocity with some random perturbation orbital_speed = np.sqrt(G * black_hole.mass / distance) - perturbation = np.random.uniform(0.95, 1.05) # Small randomness + perturbation = np.random.uniform(0.95, 1.05) vx = -orbital_speed * np.sin(angle) * perturbation vz = orbital_speed * np.cos(angle) * perturbation vy = np.random.uniform(-orbital_speed/20, orbital_speed/20) - # Color based on distance from black hole (redshift effect) distance_ratio = (distance - inner_radius) / (outer_radius - inner_radius) - # Hotter (blue) near the center, cooler (red) further out r = min(1.0, 0.2 + distance_ratio * 0.8) g = max(0.0, 0.5 - distance_ratio * 0.5) b = max(0.0, 1.0 - distance_ratio * 0.8) @@ -63,8 +61,7 @@ def main(): print(f"Added {len(particle_system.particles)} particles") - # Add some particles with high velocity for jet effects - for i in range(30): + for _ in range(30): angle = np.random.uniform(0, 2 * np.pi) distance = inner_radius * 1.2 x = distance * np.cos(angle) @@ -72,7 +69,6 @@ def main(): y_sign = 1 if np.random.random() > 0.5 else -1 y = y_sign * distance * 0.3 - # High velocity in y-direction for jet effect vx = np.random.uniform(-2e7, 2e7) vz = np.random.uniform(-2e7, 2e7) vy = np.random.uniform(8e7, 2e8) * y_sign @@ -81,17 +77,18 @@ def main(): mass=np.random.uniform(1e8, 1e9), position=(x, y, z), velocity=(vx, vy, vz), - colour=(0.7, 0.7, 1.0) # Bluish color for jets + colour=(0.7, 0.7, 1.0) ) clock = pygame.time.Clock() running = True frame_count = 0 - time_step = 50.0 # Reduced time step for better stability + time_step = 50.0 - # Main simulation loop while running: - dt = clock.tick(60) / 1000.0 # Delta time in seconds + dt = clock.tick(60) / 1000.0 + if frame_count % 60 == 0: + print(f"Frame {frame_count}, particles: {len(particle_system.particles)}") for event in pygame.event.get(): if event.type == pygame.QUIT: @@ -103,27 +100,20 @@ def main(): particle_system.grav_enabled = not particle_system.grav_enabled print(f"Gravity {'enabled' if particle_system.grav_enabled else 'disabled'}") elif event.key == pygame.K_r: - # Reset camera camera.position = np.array([0.0, 2e11, -3e11], dtype=np.float64) camera.zoom = 1.0 print("Camera reset") - # Handle camera controls camera.handle_event(event, dt) - # Handle continuous key presses for smoother camera movement keys = pygame.key.get_pressed() camera.handle_continuous_movement(keys, dt) - # Clear screen - screen.fill((0, 0, 0)) # Black background + screen.fill((0, 0, 0)) - # Update physics particle_system.update(black_hole, time_step) - # add new particles if frame_count % 120 == 0 and len(particle_system.particles) < num_particles * 0.9: - # Add a new particle to the accretion disk distance = np.random.uniform(inner_radius * 1.5, outer_radius) angle = np.random.uniform(0, 2 * np.pi) x = distance * np.cos(angle) @@ -146,12 +136,10 @@ def main(): colour=(r, g, b) ) - # Render try: renderer.render(black_hole, particle_system, camera) except Exception as e: print(f"Rendering error: {e}") - # Fallback rendering pygame.draw.circle(screen, (255, 0, 0), (width//2, height//2), 50) pygame.display.flip() diff --git a/src/physics/black_hole.py b/src/physics/black_hole.py index 061b347..01b11d0 100644 --- a/src/physics/black_hole.py +++ b/src/physics/black_hole.py @@ -4,24 +4,20 @@ class BlackHole: def __init__(self, mass, position = (0,0,0)): - """initialise black hole with mass (kg) position (m) and calculate schwarzschild radius (m)""" self.position = np.array(position,dtype=np.float32) self.mass = mass self.schwarz_radius = 2 * G * mass / (C ** 2) def get_rad_vec(self, position): - """get vector from black hole to position""" return self.position - position def get_rad(self, r_vec): - """get distance from black hole to position""" r_vec = np.array(r_vec, dtype=np.float64) relative_pos = r_vec - self.position return np.linalg.norm(relative_pos) def calc_grav_accel(self, position): - """calculate gravitational acceleration at position due to black hole""" position = np.array(position, dtype=np.float64) r_vec = self.get_rad_vec(position) @@ -31,11 +27,10 @@ def calc_grav_accel(self, position): return np.array([0.0, 0.0, 0.0]) accel = G * self.mass / (r ** 2) - direction = -r_vec / r + direction = r_vec / r return accel * direction def is_inside_horizon(self, position): - """check if position is inside event horizon""" r = self.get_rad(position) return r <= self.schwarz_radius diff --git a/src/physics/constants.py b/src/physics/constants.py index 0e8f8ba..0d51643 100644 --- a/src/physics/constants.py +++ b/src/physics/constants.py @@ -1,5 +1,5 @@ -G = 6.67430e-11 # Gravitational constant -C = 299792458.0 # Speed of light +G = 6.67430e-11 +C = 299792458.0 -bhmass = 5.972e30 # Mass of black hole in kg (approx 3 solar masses) -bhinitpos = (0, 0, 0) # Initial position of black hole \ No newline at end of file +bhmass = 5.972e30 +bhinitpos = (0, 0, 0) \ No newline at end of file diff --git a/src/physics/particle.py b/src/physics/particle.py index bc7618f..dd1afe0 100644 --- a/src/physics/particle.py +++ b/src/physics/particle.py @@ -3,7 +3,6 @@ class Particle: def __init__(self, mass, position, velocity, colour): - """initialise particle with mass (kg), position (m), velocity (m/s), and colour (r,g,b)""" self.mass = mass self.position = np.array(position, dtype=np.float64) self.velocity = np.array(velocity, dtype=np.float64) @@ -13,9 +12,8 @@ def __init__(self, mass, position, velocity, colour): self.max_trail_length = 50 def update(self, acceleration, dt): - """update particle position and velocity based on acceleration (m/s^2) and timestep dt (s)""" acceleration = np.array(acceleration, dtype=np.float64) - if acceleration.shape == (): # if acceleration is a scalar(one value) + if acceleration.shape == (): if np.linalg.norm(self.position) > 0: direction = -self.position / np.linalg.norm(self.position) acceleration = acceleration * direction @@ -38,7 +36,6 @@ def update(self, acceleration, dt): class ParticleSystem: def __init__(self): - """initialise empty particle system""" self.particles = [] self.grav_enabled = True @@ -46,17 +43,14 @@ def add_particle(self, mass, position, velocity, colour): self.particles.append(Particle(mass, position, velocity, colour)) def update(self, black_hole, dt): - """update all particles in the system based on gravitational attraction to black hole and timestep dt (s)""" for particle in self.particles: - if self.grav_enabled: # Fixed attribute name + if self.grav_enabled: accel = black_hole.calc_grav_accel(particle.position) particle.update(accel, dt) else: - # If gravity is disabled, update with zero acceleration particle.update(np.array([0.0, 0.0, 0.0]), dt) pos = np.array(particle.position, dtype=np.float64) - # if inside event horizon, change color to black if black_hole.is_inside_horizon(pos): particle.colour = (0, 0, 0) diff --git a/src/physics/physics.md b/src/physics/physics.md new file mode 100644 index 0000000..f9256a8 --- /dev/null +++ b/src/physics/physics.md @@ -0,0 +1,60 @@ +# Physics Module Documentation + +## Overview +The physics module implements the core simulation logic for a black hole and particle system using Newtonian gravity. It includes classes for black holes, particles, and particle systems, along with physical constants. + +## black_hole.py + +The `BlackHole` class simulates a black hole in 3D space, calculating gravitational effects and event horizon. + +### Class: BlackHole + +- **__init__(self, mass, position = (0,0,0))**: Initializes the black hole with mass (kg) and position (m). Computes Schwarzschild radius \( r_s = \frac{2GM}{c^2} \). + +- **get_rad_vec(self, position)**: Returns vector from black hole to given position. + +- **get_rad(self, r_vec)**: Calculates radial distance from black hole. + +- **calc_grav_accel(self, position)**: Computes gravitational acceleration at position using \( a = \frac{GM}{r^2} \), directed toward black hole. Handles r=0 case. + +- **is_inside_horizon(self, position)**: Checks if position is within Schwarzschild radius. + +This class provides gravitational field calculations for the simulation. + +## constants.py + +Defines key physical constants: + +- **G = 6.67430e-11**: Gravitational constant (m³ kg⁻¹ s⁻²). + +- **C = 299792458.0**: Speed of light (m/s). + +- **bhmass = 5.972e30**: Default black hole mass (kg, ~3 solar masses). + +- **bhinitpos = (0, 0, 0)**: Default black hole position. + +Used in gravitational and relativistic calculations. + +## particle.py + +Implements particle dynamics under gravity. + +### Class: Particle + +- **__init__(self, mass, position, velocity, colour)**: Initializes particle with mass, position, velocity, color. Maintains trail of positions for rendering. + +- **update(self, acceleration, dt)**: Updates velocity and position via Euler integration. Manages trail length. + +### Class: ParticleSystem + +- **__init__(self)**: Empty particle list, gravity enabled. + +- **add_particle(self, mass, position, velocity, colour)**: Adds particle to system. + +- **update(self, black_hole, dt)**: Updates all particles with gravitational acceleration from black hole. Changes color if inside horizon. + +Manages collection of particles and their interactions. + +## __init__.py + +Marks directory as Python package (empty). \ No newline at end of file diff --git a/src/rendering/camera.py b/src/rendering/camera.py index 812a8c0..f28c939 100644 --- a/src/rendering/camera.py +++ b/src/rendering/camera.py @@ -12,105 +12,83 @@ def __init__(self, position=None): self.target = np.array([0.0, 0.0, 0.0], dtype=np.float64) self.up = np.array([0.0, 1.0, 0.0], dtype=np.float64) - # Camera controls self.move_speed = cmove self.zoom_speed = cspeed self.zoom = czoom self.rotation_speed = crot - # For smooth movement self.velocity = np.zeros(3, dtype=np.float64) self.acceleration = 5e9 self.damping = 0.9 def handle_event(self, event, dt): - """Handle camera control events""" if event.type == pygame.KEYDOWN: if event.key == pygame.K_r: - # Reset camera self.position = np.array([0.0, 1e12, -1e12], dtype=np.float64) self.zoom = 1.0 elif event.key == pygame.K_PLUS or event.key == pygame.K_EQUALS: - # Zoom in self.zoom *= 1.0 + self.zoom_speed elif event.key == pygame.K_MINUS: - # Zoom out self.zoom /= 1.0 + self.zoom_speed def handle_continuous_movement(self, keys, dt): - """Handle continuous key presses for camera movement""" move_vector = np.zeros(3, dtype=np.float64) if keys[pygame.K_w]: - move_vector[2] += 1 # Forward - move_vector[2] -= 1 # Backward + move_vector[2] += 1 + move_vector[2] -= 1 if keys[pygame.K_a]: - move_vector[0] -= 1 # Left + move_vector[0] -= 1 if keys[pygame.K_d]: - move_vector[0] += 1 # Right + move_vector[0] += 1 if keys[pygame.K_q]: - move_vector[1] += 1 # Up + move_vector[1] += 1 if keys[pygame.K_e]: - move_vector[1] -= 1 # Down + move_vector[1] -= 1 - # Normalize if moving diagonally norm = np.linalg.norm(move_vector) if norm > 0: move_vector = move_vector / norm - # Calculate forward and right vectors relative to camera orientation forward = normalize(self.target - self.position) right = normalize(np.cross(forward, self.up)) - # Transform movement to world space world_move = move_vector[0] * right + move_vector[1] * self.up + move_vector[2] * forward - # Apply movement if np.any(world_move != 0): self.velocity += world_move * self.acceleration * dt else: - # Apply damping when not moving self.velocity *= self.damping - # Update position self.position += self.velocity * dt - # Keep camera at a minimum distance from the black hole min_distance = 1e15 distance_to_black_hole = np.linalg.norm(self.position) if distance_to_black_hole < min_distance: direction = normalize(self.position) self.position = direction * min_distance - # Reflect velocity away from black hole self.velocity -= 2 * np.dot(self.velocity, direction) * direction def world_to_screen(self, world_pos, screen_width, screen_height): - """Convert world coordinates to screen coordinates""" world_pos = np.array(world_pos, dtype=np.float64) - # Calculate relative position to camera relative_pos = world_pos - self.position - # Simple projection (assuming camera looks along forward vector) forward = normalize(self.target - self.position) - # Project onto camera plane (simplified) if np.dot(relative_pos, forward) <= 0: - return None # Behind camera + return None - # Scale based on distance and zoom scale = 1e9 * self.zoom x = relative_pos[0] / scale y = relative_pos[1] / scale - # Convert to screen coordinates screen_x = screen_width / 2 + x screen_y = screen_height / 2 - y return (int(screen_x), int(screen_y)) def normalize(vector): - """Normalize a vector""" norm = np.linalg.norm(vector) if norm == 0: return vector diff --git a/src/rendering/camera_config.py b/src/rendering/camera_config.py index 4a3ab9d..4848e91 100644 --- a/src/rendering/camera_config.py +++ b/src/rendering/camera_config.py @@ -1,5 +1,5 @@ -cdist = 1e15 # Default camera distance from black hole -cspeed = 1e15 # Camera movement speed -czoom = 0.5 # Camera zoom speed -cmove = 5e13 # Camera move speed -crot = 1 # Camera rotation speed \ No newline at end of file +cdist = 1e15 +cspeed = 1e15 +czoom = 0.5 +cmove = 5e13 +crot = 1 \ No newline at end of file diff --git a/src/rendering/renderer.py b/src/rendering/renderer.py index fa65c7c..df9edf5 100644 --- a/src/rendering/renderer.py +++ b/src/rendering/renderer.py @@ -8,14 +8,11 @@ def __init__(self, width, height, screen): self.screen = screen self.font = pygame.font.Font(None, 24) - # Create surface for glow effects self.glow_surface = pygame.Surface((width, height), pygame.SRCALPHA) - # Precompute accretion disk texture self.disk_texture = self.create_disk_texture(256) def create_disk_texture(self, size): - """Create a texture for the accretion disk""" texture = pygame.Surface((size, size), pygame.SRCALPHA) center = size // 2 @@ -26,7 +23,6 @@ def create_disk_texture(self, size): distance = np.sqrt(dx*dx + dy*dy) / center if distance < 1.0: - # Color based on distance (red to blue) r = min(255, int(100 + 155 * (1 - distance))) g = max(0, int(50 + 100 * (1 - distance))) b = max(0, int(50 + 205 * distance)) @@ -37,36 +33,27 @@ def create_disk_texture(self, size): return texture def render(self, black_hole, particle_system, camera): - """Render the black hole simulation""" - # Clear screen + print("Rendering frame...") self.screen.fill((0, 0, 0)) self.glow_surface.fill((0, 0, 0, 0)) - # Render black hole with accretion disk self.render_black_hole(black_hole, camera) - # Render particles self.render_particles(particle_system, camera) - # Apply glow effect self.apply_glow() - # Render UI self.render_ui(camera, particle_system, black_hole) def render_black_hole(self, black_hole, camera): - """Render the black hole with detailed accretion disk""" - # Convert black hole position to screen coordinates screen_pos = camera.world_to_screen(black_hole.position, self.width, self.height) + print(f"Black hole screen position: {screen_pos}") if screen_pos: - # Calculate sizes based on Schwarzschild radius and zoom schwarz_radius_px = max(5, int(black_hole.schwarz_radius / (1e9 * camera.zoom))) disk_radius_px = schwarz_radius_px * 6 - # Draw accretion disk if disk_radius_px > 5: - # Create a scaled version of the disk texture scaled_disk = pygame.transform.scale( self.disk_texture, (disk_radius_px * 2, disk_radius_px * 2) @@ -74,69 +61,55 @@ def render_black_hole(self, black_hole, camera): disk_rect = scaled_disk.get_rect(center=screen_pos) self.screen.blit(scaled_disk, disk_rect) - # Draw photon sphere (just inside the event horizon) photon_radius = int(schwarz_radius_px * 1.5) if photon_radius > 2: pygame.draw.circle(self.glow_surface, (150, 150, 255, 100), screen_pos, photon_radius, 2) - # Draw event horizon if schwarz_radius_px > 2: - # Draw a dark circle for the event horizon pygame.draw.circle(self.screen, (10, 10, 10), screen_pos, schwarz_radius_px) - # Add a slight glow around the event horizon pygame.draw.circle(self.glow_surface, (50, 50, 150, 50), screen_pos, schwarz_radius_px + 2) - # Draw black hole shadow (larger than event horizon) shadow_radius = int(schwarz_radius_px * 2.5) if shadow_radius > 5: pygame.draw.circle(self.screen, (0, 0, 0), screen_pos, shadow_radius) - # Add gravitational lensing effect (distortion ring) for i in range(3): ring_radius = shadow_radius + 5 + i * 3 pygame.draw.circle(self.glow_surface, (100, 100, 200, 30), screen_pos, ring_radius, 1) def render_particles(self, particle_system, camera): - """Render all particles with glow effects""" for particle in particle_system.particles: screen_pos = camera.world_to_screen(particle.position, self.width, self.height) if screen_pos: - # Convert color from (0,1) range to (0,255) range color = ( int(particle.colour[0] * 255), int(particle.colour[1] * 255), int(particle.colour[2] * 255) ) - # Calculate particle size based on mass particle_size = max(1, int(2 + np.log10(particle.mass) / 2)) - # Draw glow effect glow_radius = particle_size * 3 glow_color = (color[0], color[1], color[2], 100) pygame.draw.circle(self.glow_surface, glow_color, screen_pos, glow_radius) - # Draw particle pygame.draw.circle(self.screen, color, screen_pos, particle_size) - # Draw trail if it exists if len(particle.trail) > 1: self.render_trail(particle, camera) def render_trail(self, particle, camera): - """Render particle trail""" trail_points = [] - for i, trail_pos in enumerate(particle.trail[-15:]): # Last 15 trail points + for i, trail_pos in enumerate(particle.trail[-15:]): trail_screen_pos = camera.world_to_screen(trail_pos, self.width, self.height) if trail_screen_pos: trail_points.append(trail_screen_pos) if len(trail_points) > 1: - # Draw trail as connected lines with fading effect for i in range(len(trail_points) - 1): - alpha = int(200 * (i + 1) / len(trail_points)) # Fade from transparent to opaque + alpha = int(200 * (i + 1) / len(trail_points)) trail_color = ( int(particle.colour[0] * 255), int(particle.colour[1] * 255), @@ -146,35 +119,27 @@ def render_trail(self, particle, camera): pygame.draw.line(self.glow_surface, trail_color, trail_points[i], trail_points[i + 1], 2) def apply_glow(self): - """Apply glow effect by blurring the glow surface and combining with main screen""" - # Simple blur effect by scaling down and up small = pygame.transform.smoothscale(self.glow_surface, (self.width//4, self.height//4)) blurred = pygame.transform.smoothscale(small, (self.width, self.height)) self.screen.blit(blurred, (0, 0), special_flags=pygame.BLEND_ADD) def render_ui(self, camera, particle_system, black_hole): - """Render UI information""" - # Camera info cam_text = f"Camera: ({camera.position[0]:.2e}, {camera.position[1]:.2e}, {camera.position[2]:.2e})" cam_surface = self.font.render(cam_text, True, (255, 255, 255)) self.screen.blit(cam_surface, (10, 10)) - # Zoom info zoom_text = f"Zoom: {camera.zoom:.2f}" zoom_surface = self.font.render(zoom_text, True, (255, 255, 255)) self.screen.blit(zoom_surface, (10, 40)) - # Particle count particle_text = f"Particles: {len(particle_system.particles)}" particle_surface = self.font.render(particle_text, True, (255, 255, 255)) self.screen.blit(particle_surface, (10, 70)) - # Gravity status gravity_text = f"Gravity: {'ON' if particle_system.grav_enabled else 'OFF'} (G to toggle)" gravity_surface = self.font.render(gravity_text, True, (255, 255, 255)) self.screen.blit(gravity_surface, (10, 100)) - # Black hole info bh_text = f"BH Mass: {black_hole.mass:.2e} kg" bh_surface = self.font.render(bh_text, True, (255, 255, 255)) self.screen.blit(bh_surface, (10, 130)) @@ -183,7 +148,6 @@ def render_ui(self, camera, particle_system, black_hole): rs_surface = self.font.render(rs_text, True, (255, 255, 255)) self.screen.blit(rs_surface, (10, 160)) - # Controls controls = [ "Controls:", "WASD - Move camera", diff --git a/src/rendering/rendering.md b/src/rendering/rendering.md new file mode 100644 index 0000000..85d4b5e --- /dev/null +++ b/src/rendering/rendering.md @@ -0,0 +1,66 @@ +# Rendering Module Documentation + +## Overview +The rendering module handles visualization of the black hole simulation using Pygame. It includes camera controls, rendering of black holes, particles, and UI elements. + +## camera.py + +The `Camera` class manages the viewpoint and user controls for navigating the 3D simulation in 2D screen space. + +### Class: Camera + +- **__init__(self, position=None)**: Initializes camera at default or given position, with target at origin and up vector (0,1,0). Sets movement, zoom, rotation speeds from config. + +- **handle_event(self, event, dt)**: Processes discrete events like key presses for reset (R), zoom in/out (+/-). + +- **handle_continuous_movement(self, keys, dt)**: Handles continuous key presses (WASDQE) for movement. Calculates world-space movement vectors, applies velocity with damping, updates position. Prevents camera from getting too close to black hole. + +- **world_to_screen(self, world_pos, screen_width, screen_height)**: Projects 3D world position to 2D screen coordinates using simple perspective projection based on camera forward vector and zoom. + +- **normalize(vector)**: Utility function to normalize vectors, handling zero norm case. + +Provides smooth camera movement and projection for rendering. + +## camera_config.py + +Defines camera configuration parameters: + +- **cdist = 1e15**: Default distance from black hole. + +- **cspeed = 1e15**: Movement speed. + +- **czoom = 0.5**: Zoom speed multiplier. + +- **cmove = 5e13**: Move speed. + +- **crot = 1**: Rotation speed. + +These are used to tune camera behavior. + +## renderer.py + +The `Renderer` class handles all drawing operations, including black hole, particles, trails, glow effects, and UI. + +### Class: Renderer + +- **__init__(self, width, height, screen)**: Initializes with screen dimensions, font, glow surface, and precomputes accretion disk texture. + +- **create_disk_texture(self, size)**: Generates a radial gradient texture for the accretion disk, with colors varying by distance (red to blue). + +- **render(self, black_hole, particle_system, camera)**: Main render loop: clears screen, renders black hole, particles, applies glow, renders UI. + +- **render_black_hole(self, black_hole, camera)**: Renders black hole with accretion disk (scaled texture), photon sphere, event horizon (dark circle), shadow, and lensing rings. + +- **render_particles(self, particle_system, camera)**: Renders each particle as a circle with size based on mass, glow effect, and trail if present. + +- **render_trail(self, particle, camera)**: Draws particle trail as fading lines on glow surface. + +- **apply_glow(self)**: Blurs glow surface and blends onto main screen for lighting effects. + +- **render_ui(self, camera, particle_system, black_hole)**: Displays camera position, zoom, particle count, gravity status, black hole mass, Schwarzschild radius, and controls help. + +Manages visual representation with effects like glow and trails. + +## __init__.py + +Marks directory as Python package (empty). \ No newline at end of file diff --git a/src/utils/utils.md b/src/utils/utils.md new file mode 100644 index 0000000..09eec53 --- /dev/null +++ b/src/utils/utils.md @@ -0,0 +1,18 @@ +# Utils Module Documentation + +## Overview +The utils module provides utility functions for vector mathematics used in physics and rendering. + +## vectors.py + +Contains functions for common vector operations. + +- **normalize(vector)**: Normalizes a vector to unit length. If norm is zero, returns the original vector to avoid division by zero. + +- **rotate_vector(vector, axis, angle)**: Rotates a vector around an axis by a given angle using Rodrigues' rotation formula: \( v' = v \cos\theta + (k \times v) \sin\theta + k (k \cdot v) (1 - \cos\theta) \), where k is the unit axis. + +These functions support 3D transformations in the simulation. + +## __init__.py + +Marks directory as Python package (empty). \ No newline at end of file diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..0874bcb --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,47 @@ +import pytest +import numpy as np +import sys +import os + +# Add src to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from physics.black_hole import BlackHole +from physics.particle import ParticleSystem +from rendering.camera import Camera +from rendering.renderer import Renderer + +class TestIntegration: + def test_black_hole_particle_interaction(self): + bh = BlackHole(mass=1e30, position=(0, 0, 0)) + ps = ParticleSystem() + ps.add_particle(1e10, (5, 0, 0), (0, 0, 0), (1, 0, 0)) + + initial_pos = ps.particles[0].position.copy() + ps.update(bh, 1.0) + + # Particle should move toward black hole + assert ps.particles[0].position[0] < initial_pos[0] + + def test_camera_projection(self): + cam = Camera(position=(0, 0, 10)) + bh = BlackHole(mass=1e30, position=(0, 0, 0)) + + screen_pos = cam.world_to_screen(bh.position, 800, 600) + assert screen_pos == (400, 300) + + def test_full_simulation_step(self): + bh = BlackHole(mass=1e30) + ps = ParticleSystem() + ps.add_particle(1e10, (5, 0, 0), (0, 0, 0), (1, 0, 0)) + cam = Camera() + + # Simulate one update step + ps.update(bh, 1.0) + + # Check particle moved + assert not np.array_equal(ps.particles[0].position, (5, 0, 0)) + + # Camera should still work + screen_pos = cam.world_to_screen(ps.particles[0].position, 800, 600) + assert screen_pos is not None \ No newline at end of file diff --git a/tests/test_physics.py b/tests/test_physics.py new file mode 100644 index 0000000..672361a --- /dev/null +++ b/tests/test_physics.py @@ -0,0 +1,114 @@ +import pytest +import numpy as np +import sys +import os + +# Add src to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from physics.black_hole import BlackHole +from physics.particle import Particle, ParticleSystem +from physics.constants import G, C + +class TestBlackHole: + def test_init(self): + bh = BlackHole(mass=1e30, position=(1, 2, 3)) + assert bh.mass == 1e30 + assert np.array_equal(bh.position, (1, 2, 3)) + expected_rs = 2 * G * 1e30 / (C ** 2) + assert bh.schwarz_radius == expected_rs + + def test_get_rad_vec(self): + bh = BlackHole(mass=1e30, position=(0, 0, 0)) + r_vec = bh.get_rad_vec((3, 4, 0)) + assert np.array_equal(r_vec, (-3, -4, 0)) + + def test_get_rad(self): + bh = BlackHole(mass=1e30, position=(0, 0, 0)) + r = bh.get_rad((3, 4, 0)) + assert r == 5.0 + + def test_calc_grav_accel(self): + bh = BlackHole(mass=1e30, position=(0, 0, 0)) + accel = bh.calc_grav_accel((5, 0, 0)) + expected_mag = G * 1e30 / 25 + assert abs(np.linalg.norm(accel) - expected_mag) < 1e-10 + assert np.array_equal(accel, (-expected_mag, 0, 0)) # Direction toward BH + + def test_calc_grav_accel_at_center(self): + bh = BlackHole(mass=1e30, position=(0, 0, 0)) + accel = bh.calc_grav_accel((0, 0, 0)) + assert np.array_equal(accel, (0, 0, 0)) + + def test_is_inside_horizon(self): + bh = BlackHole(mass=1e30) + assert bh.is_inside_horizon((0, 0, 0)) # At center + assert not bh.is_inside_horizon((2000, 0, 0)) # Outside + +class TestParticle: + def test_init(self): + p = Particle(mass=1e10, position=(1, 2, 3), velocity=(0, 0, 0), colour=(1, 0, 0)) + assert p.mass == 1e10 + assert np.array_equal(p.position, (1, 2, 3)) + assert np.array_equal(p.velocity, (0, 0, 0)) + assert p.colour == (1, 0, 0) + assert len(p.trail) == 0 + + def test_update_vector_accel(self): + p = Particle(mass=1e10, position=(0, 0, 0), velocity=(1, 0, 0), colour=(1, 0, 0)) + accel = np.array([0, 1, 0]) + p.update(accel, 1.0) + assert np.array_equal(p.velocity, (1, 1, 0)) + assert np.array_equal(p.position, (1, 1, 0)) + assert len(p.trail) == 1 + + def test_update_scalar_accel(self): + p = Particle(mass=1e10, position=(3, 0, 0), velocity=(0, 0, 0), colour=(1, 0, 0)) + p.update(1.0, 1.0) # Scalar accel toward center + expected_vel = np.array([-1, 0, 0]) # Direction normalized + assert np.allclose(p.velocity, expected_vel, atol=1e-10) + + def test_trail_management(self): + p = Particle(mass=1e10, position=(0, 0, 0), velocity=(0, 0, 0), colour=(1, 0, 0)) + for i in range(60): + p.update((0, 0, 0), 1.0) + assert len(p.trail) == 50 # Max length + +class TestParticleSystem: + def test_init(self): + ps = ParticleSystem() + assert len(ps.particles) == 0 + assert ps.grav_enabled + + def test_add_particle(self): + ps = ParticleSystem() + ps.add_particle(1e10, (0, 0, 0), (0, 0, 0), (1, 0, 0)) + assert len(ps.particles) == 1 + assert ps.particles[0].mass == 1e10 + + def test_update_with_gravity(self): + bh = BlackHole(mass=1e30, position=(0, 0, 0)) + ps = ParticleSystem() + p = Particle(mass=1e10, position=(5, 0, 0), velocity=(0, 0, 0), colour=(1, 0, 0)) + ps.add_particle(p.mass, p.position, p.velocity, p.colour) + ps.update(bh, 1.0) + # Particle should accelerate toward BH + assert ps.particles[0].velocity[0] < 0 + + def test_update_without_gravity(self): + bh = BlackHole(mass=1e30, position=(0, 0, 0)) + ps = ParticleSystem() + ps.grav_enabled = False + p = Particle(mass=1e10, position=(0, 0, 0), velocity=(1, 0, 0), colour=(1, 0, 0)) + ps.add_particle(p.mass, p.position, p.velocity, p.colour) + ps.update(bh, 1.0) + # No acceleration + assert np.array_equal(ps.particles[0].velocity, (1, 0, 0)) + + def test_horizon_color_change(self): + bh = BlackHole(mass=1e30, position=(0, 0, 0)) # Large BH + ps = ParticleSystem() + p = Particle(mass=1e10, position=(0, 0, 0), velocity=(0, 0, 0), colour=(1, 0, 0)) + ps.add_particle(p.mass, p.position, p.velocity, p.colour) + ps.update(bh, 1.0) + assert ps.particles[0].colour == (0, 0, 0) # Black inside horizon \ No newline at end of file diff --git a/tests/test_rendering.py b/tests/test_rendering.py new file mode 100644 index 0000000..418f765 --- /dev/null +++ b/tests/test_rendering.py @@ -0,0 +1,72 @@ +import pytest +import numpy as np +import sys +import os + +# Add src to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from rendering.camera import Camera +from rendering.camera_config import cdist, cspeed, czoom, cmove, crot +from rendering.renderer import Renderer +from physics.black_hole import BlackHole +from physics.particle import ParticleSystem + +class TestCamera: + def test_init_default(self): + cam = Camera() + expected_pos = np.array([0.0, cdist, -cdist]) + assert np.array_equal(cam.position, expected_pos) + assert np.array_equal(cam.target, (0, 0, 0)) + assert np.array_equal(cam.up, (0, 1, 0)) + assert cam.move_speed == cmove + assert cam.zoom_speed == cspeed + assert cam.zoom == czoom + + def test_init_custom(self): + pos = (1, 2, 3) + cam = Camera(position=pos) + assert np.array_equal(cam.position, pos) + + def test_world_to_screen(self): + cam = Camera(position=(0, 0, 10)) + screen_pos = cam.world_to_screen((0, 0, 0), 800, 600) + assert screen_pos == (400, 300) # Center of screen + + def test_world_to_screen_behind(self): + cam = Camera(position=(0, 0, 10)) + screen_pos = cam.world_to_screen((0, 0, 20), 800, 600) + assert screen_pos is None # Behind camera + +class TestRenderer: + @pytest.fixture + def renderer(self): + import pygame + pygame.init() + screen = pygame.display.set_mode((800, 600)) + yield Renderer(800, 600, screen) + pygame.quit() + + def test_init(self, renderer): + assert renderer.width == 800 + assert renderer.height == 600 + assert renderer.font is not None + assert renderer.glow_surface is not None + assert renderer.disk_texture is not None + + def test_create_disk_texture(self, renderer): + texture = renderer.create_disk_texture(64) + assert texture.get_size() == (64, 64) + # Check center pixel (should be bright) + center_color = texture.get_at((32, 32)) + assert center_color[3] > 0 # Alpha > 0 + # Check edge pixel (should be darker) + edge_color = texture.get_at((0, 0)) + assert edge_color[3] == 0 # Transparent at edge + + def test_render_black_hole(self, renderer): + bh = BlackHole(mass=1e30) + cam = Camera() + # This would require a mock screen, but for now, assume it doesn't crash + # In real test, use pygame.display.set_mode with no frame + pass # Skip detailed test due to pygame dependency \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..7ff9be4 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,34 @@ +import pytest +import numpy as np +import sys +import os + +# Add src to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from utils.vectors import normalize, rotate_vector + +class TestVectors: + def test_normalize(self): + vec = np.array([3, 4, 0]) + norm_vec = normalize(vec) + assert np.allclose(np.linalg.norm(norm_vec), 1.0) + assert np.allclose(norm_vec, (0.6, 0.8, 0)) + + def test_normalize_zero(self): + vec = np.array([0, 0, 0]) + norm_vec = normalize(vec) + assert np.array_equal(norm_vec, (0, 0, 0)) + + def test_rotate_vector_x_axis(self): + vec = np.array([0, 0, 1]) + axis = np.array([1, 0, 0]) + rotated = rotate_vector(vec, axis, np.pi / 2) + expected = np.array([0, -1, 0]) + assert np.allclose(rotated, expected, atol=1e-10) + + def test_rotate_vector_identity(self): + vec = np.array([1, 0, 0]) + axis = np.array([0, 1, 0]) + rotated = rotate_vector(vec, axis, 0) + assert np.allclose(rotated, vec) \ No newline at end of file