From 022d1d209ec1048c8682cf071a0b04319839ae84 Mon Sep 17 00:00:00 2001 From: Ryan Ramboer Date: Wed, 6 May 2026 12:41:52 -0500 Subject: [PATCH 1/2] chore: prep for first PyPI release (v0.1.0) - Set version to 0.1.0; classifier to Alpha (more honest first release) - Replace yourusername placeholder URLs with rramboer - Consolidate tooling on Ruff (drop black, isort, bandit redundancy) - Drop duplicate requirements*.txt; pyproject.toml is sole source of truth - Remove dead IntegrationMethod config option (was never wired into Rocket.update) - Document that Rocket.update uses symplectic Euler / Euler-Cromer - Update README/CONTRIBUTING/CHANGELOG to match new tooling + version - Add Python 3.13 to CI matrix; drop reference to nonexistent develop branch - Add pip caching to CI; install dev extras instead of hardcoded tool versions --- .github/workflows/ci.yml | 41 ++++++++++++++++-------------------- .pre-commit-config.yaml | 26 ++--------------------- CHANGELOG.md | 29 +++++++++++++------------ CONTRIBUTING.md | 20 ++++++++---------- README.md | 29 ++++++++++++------------- pyproject.toml | 30 ++++++++------------------ requirements-dev.txt | 26 ----------------------- requirements.txt | 5 ----- src/rocket_sim/__init__.py | 2 +- src/rocket_sim/config.py | 16 -------------- src/rocket_sim/models.py | 6 ++++-- src/rocket_sim/simulation.py | 2 +- 12 files changed, 72 insertions(+), 160 deletions(-) delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5697c1..d55c364 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main, develop] + branches: [main] pull_request: branches: [main] @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - name: Checkout code @@ -23,14 +23,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - - name: Cache pip packages - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-pip- + cache: pip + cache-dependency-path: pyproject.toml - name: Install dependencies run: | @@ -38,10 +32,10 @@ jobs: pip install -e ".[dev]" - name: Run tests with coverage - run: | - pytest --cov=rocket_sim --cov-report=xml --cov-report=term-missing + run: pytest --cov=rocket_sim --cov-report=xml --cov-report=term-missing - name: Upload coverage to Codecov + if: matrix.python-version == '3.12' uses: codecov/codecov-action@v4 with: file: ./coverage.xml @@ -58,22 +52,21 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" + cache: pip + cache-dependency-path: pyproject.toml - name: Install dependencies run: | python -m pip install --upgrade pip - pip install ruff black isort - - - name: Check formatting with Black - run: black --check src tests - - - name: Check import sorting with isort - run: isort --check-only src tests + pip install -e ".[dev]" - - name: Lint with Ruff + - name: Ruff lint run: ruff check src tests + - name: Ruff format check + run: ruff format --check src tests + type-check: name: Type Check runs-on: ubuntu-latest @@ -85,7 +78,9 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" + cache: pip + cache-dependency-path: pyproject.toml - name: Install dependencies run: | @@ -107,7 +102,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - name: Install build tools run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1ec4f99..6738a02 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,26 +17,13 @@ repos: - id: detect-private-key - id: debug-statements - # Python code formatting - - repo: https://github.com/psf/black - rev: 24.1.1 - hooks: - - id: black - language_version: python3 - - # Import sorting - - repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort - args: ["--profile", "black"] - - # Linting with Ruff + # Lint + format with Ruff (replaces black, isort, flake8, pyupgrade, etc.) - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.1.14 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format # Type checking with mypy - repo: https://github.com/pre-commit/mirrors-mypy @@ -49,15 +36,6 @@ repos: args: [--ignore-missing-imports] exclude: tests/ - # Check for security issues - - repo: https://github.com/PyCQA/bandit - rev: 1.7.7 - hooks: - - id: bandit - args: ["-c", "pyproject.toml"] - additional_dependencies: ["bandit[toml]"] - exclude: tests/ - # Configuration ci: autofix_commit_msg: | diff --git a/CHANGELOG.md b/CHANGELOG.md index c232082..0a16a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] - 2024-01-24 +## [Unreleased] + +### Planned + +- Multi-stage rocket support +- Atmospheric drag integrated into trajectory simulation +- 3D trajectory visualization +- Real-time simulation mode +- Export to various data formats (CSV, JSON) +- Orbital insertion calculations + +## [0.1.0] + +First public release on PyPI. ### Added @@ -73,7 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - pytest test suite with fixtures - Pre-commit hooks configuration - GitHub Actions CI/CD - - Black, isort, ruff for code quality + - Ruff for linting and formatting - mypy for type checking ### Technical Details @@ -83,15 +96,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - NumPy for numerical operations - Modern Python packaging with pyproject.toml - src-layout package structure - -## [Unreleased] - -### Planned - -- Multi-stage rocket support -- Atmospheric drag simulation -- 3D trajectory visualization -- Real-time simulation mode -- Rocket builder GUI -- Export to various data formats (CSV, JSON) -- Orbital insertion calculations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6611965..cbc7a89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,9 +47,8 @@ Feature suggestions are welcome! Please open an issue with: ``` 8. **Ensure code quality**: ```bash - black src tests - isort src tests - ruff check src tests + ruff check --fix src tests + ruff format src tests mypy src ``` 9. **Commit** with a clear message: @@ -108,20 +107,19 @@ pytest -k "test_gravity" This project uses: -- **Black** for code formatting -- **isort** for import sorting -- **Ruff** for linting +- **Ruff** for linting and formatting (replaces black, isort, flake8, pyupgrade) - **mypy** for type checking All of these run automatically via pre-commit hooks, but you can also run them manually: ```bash -# Format code -black src tests -isort src tests +# Lint and auto-fix +ruff check --fix src tests -# Check for issues -ruff check src tests +# Format +ruff format src tests + +# Type check mypy src ``` diff --git a/README.md b/README.md index 83bdc8e..5277375 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Rocket Simulator -[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/) +[![PyPI](https://img.shields.io/pypi/v/rocket-sim.svg)](https://pypi.org/project/rocket-sim/) +[![Python](https://img.shields.io/pypi/pyversions/rocket-sim.svg)](https://pypi.org/project/rocket-sim/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Tests](https://img.shields.io/badge/tests-passing-brightgreen.svg)]() -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![CI](https://github.com/rramboer/rocket-sim/actions/workflows/ci.yml/badge.svg)](https://github.com/rramboer/rocket-sim/actions/workflows/ci.yml) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) A physics-based rocket trajectory simulation library for Python. Simulate rocket launches with realistic gravitational physics, visualize trajectories, and compare real-world rockets. @@ -25,14 +25,14 @@ A physics-based rocket trajectory simulation library for Python. Simulate rocket ### Installation ```bash -# Clone the repository -git clone https://github.com/yourusername/rocket-sim.git -cd rocket-sim +pip install rocket-sim +``` -# Install in development mode -pip install -e . +Or, for development: -# Or install with development dependencies +```bash +git clone https://github.com/rramboer/rocket-sim.git +cd rocket-sim pip install -e ".[dev]" ``` @@ -188,7 +188,7 @@ rocket-sim/ ```bash # Clone and install with dev dependencies -git clone https://github.com/yourusername/rocket-sim.git +git clone https://github.com/rramboer/rocket-sim.git cd rocket-sim pip install -e ".[dev]" @@ -212,12 +212,9 @@ pytest tests/test_physics.py ### Code Quality ```bash -# Format code -black src tests -isort src tests - -# Lint -ruff check src tests +# Lint and format (Ruff handles both) +ruff check --fix src tests +ruff format src tests # Type checking mypy src diff --git a/pyproject.toml b/pyproject.toml index 981db36..87ea8e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "rocket-sim" -version = "1.0.0" +version = "0.1.0" description = "A physics-based rocket trajectory simulation library" readme = "README.md" license = "MIT" @@ -21,7 +21,7 @@ keywords = [ "education", ] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", @@ -48,8 +48,6 @@ dev = [ "pytest-cov>=4.0.0", "mypy>=1.0.0", "ruff>=0.1.0", - "black>=23.0.0", - "isort>=5.12.0", "pre-commit>=3.0.0", ] docs = [ @@ -62,11 +60,11 @@ docs = [ rocket-sim = "rocket_sim.cli:cli_main" [project.urls] -Homepage = "https://github.com/yourusername/rocket-sim" -Documentation = "https://github.com/yourusername/rocket-sim#readme" -Repository = "https://github.com/yourusername/rocket-sim.git" -Issues = "https://github.com/yourusername/rocket-sim/issues" -Changelog = "https://github.com/yourusername/rocket-sim/blob/main/CHANGELOG.md" +Homepage = "https://github.com/rramboer/rocket-sim" +Documentation = "https://github.com/rramboer/rocket-sim#readme" +Repository = "https://github.com/rramboer/rocket-sim.git" +Issues = "https://github.com/rramboer/rocket-sim/issues" +Changelog = "https://github.com/rramboer/rocket-sim/blob/main/CHANGELOG.md" [tool.hatch.build.targets.sdist] include = [ @@ -105,18 +103,8 @@ ignore = [ [tool.ruff.lint.isort] known-first-party = ["rocket_sim"] -# Black - Code formatter -[tool.black] -line-length = 100 -target-version = ["py310", "py311", "py312"] -include = '\.pyi?$' - -# isort - Import sorter -[tool.isort] -profile = "black" -line_length = 100 -known_first_party = ["rocket_sim"] -src_paths = ["src", "tests"] +[tool.ruff.format] +docstring-code-format = true # MyPy - Static type checker [tool.mypy] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index d4c8a73..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,26 +0,0 @@ -# Development dependencies for rocket-sim -# Install with: pip install -r requirements-dev.txt - -# Include production dependencies --r requirements.txt - -# Testing -pytest>=7.0.0 -pytest-cov>=4.0.0 -pytest-xdist>=3.0.0 - -# Type checking -mypy>=1.0.0 - -# Linting and formatting -ruff>=0.1.0 -black>=23.0.0 -isort>=5.12.0 - -# Pre-commit hooks -pre-commit>=3.0.0 - -# Documentation (optional) -sphinx>=6.0.0 -sphinx-rtd-theme>=1.2.0 -myst-parser>=1.0.0 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 612afa1..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Production dependencies for rocket-sim -# Install with: pip install -r requirements.txt - -matplotlib>=3.7.0 -numpy>=1.24.0 diff --git a/src/rocket_sim/__init__.py b/src/rocket_sim/__init__.py index a35fc6d..58dbf4a 100644 --- a/src/rocket_sim/__init__.py +++ b/src/rocket_sim/__init__.py @@ -17,7 +17,7 @@ ) from rocket_sim.visualization import PlotOptions, PlotStyle, Plotter -__version__ = "1.0.0" +__version__ = "0.1.0" __author__ = "Ryan R" __all__ = [ # Core classes diff --git a/src/rocket_sim/config.py b/src/rocket_sim/config.py index 1c3cafe..d0b6e92 100644 --- a/src/rocket_sim/config.py +++ b/src/rocket_sim/config.py @@ -10,18 +10,10 @@ import json import logging from dataclasses import dataclass, field -from enum import Enum from pathlib import Path from typing import Any -class IntegrationMethod(Enum): - """Numerical integration methods available for simulation.""" - - EULER = "euler" - EULER_CROMER = "euler_cromer" # Symplectic Euler - better energy conservation - - @dataclass class SimulationConfig: """ @@ -30,14 +22,12 @@ class SimulationConfig: Attributes: dt: Time step for simulation in seconds. max_time: Maximum simulation duration in seconds. - integration_method: Numerical integration method to use. detect_escape: Whether to stop simulation on escape velocity. log_level: Logging verbosity level. """ dt: float = 0.1 max_time: float = 1_000_000.0 - integration_method: IntegrationMethod = IntegrationMethod.EULER detect_escape: bool = True log_level: int = logging.INFO @@ -53,7 +43,6 @@ def to_dict(self) -> dict[str, Any]: return { "dt": self.dt, "max_time": self.max_time, - "integration_method": self.integration_method.value, "detect_escape": self.detect_escape, "log_level": self.log_level, } @@ -61,14 +50,9 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls, data: dict[str, Any]) -> SimulationConfig: """Create configuration from dictionary.""" - method = data.get("integration_method", "euler") - if isinstance(method, str): - method = IntegrationMethod(method) - return cls( dt=float(data.get("dt", 0.1)), max_time=float(data.get("max_time", 1_000_000.0)), - integration_method=method, detect_escape=bool(data.get("detect_escape", True)), log_level=int(data.get("log_level", logging.INFO)), ) diff --git a/src/rocket_sim/models.py b/src/rocket_sim/models.py index 70af2d4..5130eb9 100644 --- a/src/rocket_sim/models.py +++ b/src/rocket_sim/models.py @@ -176,7 +176,9 @@ def update( Update rocket state for a time step. Computes acceleration from thrust and gravity, then updates - velocity and position using simple Euler integration. + velocity and position using symplectic Euler (Euler-Cromer) + integration, which conserves energy better than the plain + Euler method for orbital trajectories. Args: dt: Time step in seconds. @@ -197,7 +199,7 @@ def update( # Coasting: only gravity acceleration = -gravity - # Euler integration + # Symplectic Euler: update velocity first, then position with new velocity self.velocity += acceleration * dt self.altitude += self.velocity * dt self.time += dt diff --git a/src/rocket_sim/simulation.py b/src/rocket_sim/simulation.py index 1157f94..41dfb8f 100644 --- a/src/rocket_sim/simulation.py +++ b/src/rocket_sim/simulation.py @@ -252,7 +252,7 @@ def run(self) -> SimulationResult: result.flight_time = self.rocket.time logger.info( f"Escape velocity achieved at t={self.rocket.time:.2f}s, " - f"alt={state.altitude/1000:.2f}km" + f"alt={state.altitude / 1000:.2f}km" ) break From bee594f9b335cf360967f621f3beea2d638a2e68 Mon Sep 17 00:00:00 2001 From: Ryan Ramboer Date: Wed, 6 May 2026 13:11:30 -0500 Subject: [PATCH 2/2] fix: address critical PR-review findings before v0.1.0 publish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight critical issues surfaced by the comprehensive review pass on PR #1: - C1: Remove logging.basicConfig() from RocketSimulation.__init__ — library code must never reconfigure the host application's root logger. Drop the unused log_level field from SimulationConfig along with it. - C2: Remove Engine.specific_impulse — it was stored but never read by the integrator. The simulation uses a constant-mass approximation, which is now explicitly documented on Rocket and in the README's modelling caveats. - C3: Document Physics.atmospheric_density and Physics.drag_force as standalone utilities not wired into the trajectory loop. Update the README's "Atmospheric Density (optional)" claim, which was misleading. - C4: Correct wrong example values in physics.py docstrings (gravity_at_altitude(400_000): 8.676 -> 8.694; escape_velocity(400_000): 10926.5 -> 10850.5; verified against actual outputs). - C5: Delete dead PlotConfig dataclass — duplicated PlotOptions and was never imported anywhere. Removing pre-publish avoids a breaking removal later. - C6: get_preset() now returns a dataclasses.replace() copy instead of the shared global RocketConfig instance, preventing accidental mutation of PRESETS at runtime. - C7: simulate_rocket() and simulate_multiple() accept an optional `body` parameter and forward it to RocketSimulation, so the convenience API can simulate launches from the Moon, Mars, or custom bodies. - C8: Wire CelestialBody through Rocket itself (new `body` field on Rocket; Rocket.from_config and RocketSimulation pass it through). potential_energy and total_energy now use the correct body's surface gravity and radius instead of always assuming Earth. Also addresses a few important footguns flagged in the same review: - is_on_ground uses <= instead of exact float equality. - README clarifies modelling caveats (1-D vertical, constant mass, no drag) and footnotes New Shepard's T/W < 1 to explain "immediate landing" output. - Add CelestialBody and SimulationState to __init__.py.__all__ — both are part of the documented public API but were unexported. All 62 tests pass; ruff + mypy clean; build + twine check pass. --- README.md | 33 +++++++++++++------ src/rocket_sim/__init__.py | 5 ++- src/rocket_sim/cli.py | 1 - src/rocket_sim/config.py | 50 +--------------------------- src/rocket_sim/models.py | 63 +++++++++++++++++++----------------- src/rocket_sim/physics.py | 37 ++++++++++++++------- src/rocket_sim/presets.py | 8 ++--- src/rocket_sim/simulation.py | 24 +++++++------- 8 files changed, 102 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 5277375..82eaf60 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,30 @@ [![CI](https://github.com/rramboer/rocket-sim/actions/workflows/ci.yml/badge.svg)](https://github.com/rramboer/rocket-sim/actions/workflows/ci.yml) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -A physics-based rocket trajectory simulation library for Python. Simulate rocket launches with realistic gravitational physics, visualize trajectories, and compare real-world rockets. +A small, educational physics-based rocket trajectory simulation library for Python. Simulate vertical rocket launches with altitude-dependent gravity, visualize trajectories, and compare real-world rockets. ![Rocket Trajectory Simulation](docs/images/rocket_comparison.png) ## Features -- **Realistic Physics** - Altitude-dependent gravity using Newton's inverse-square law -- **Pre-configured Rockets** - 13 real-world rockets including Saturn V, Falcon 9, Starship, and more -- **Custom Rockets** - Create and simulate your own rocket designs -- **Beautiful Visualizations** - Publication-quality plots with multiple styling options -- **CLI & Library** - Use as a command-line tool or import as a Python library -- **Type-Safe** - Full type hints for excellent IDE support -- **Well-Tested** - Comprehensive test suite with pytest +- **Altitude-dependent gravity** using Newton's inverse-square law +- **13 pre-configured rockets** including Saturn V, Falcon 9, Starship, and more +- **Custom rockets** — create and simulate your own designs +- **Multiple celestial bodies** — Earth, Moon, and Mars, or roll your own +- **Plots** with multiple matplotlib styles (trajectory, velocity, dashboard, comparison) +- **CLI & library** — use as a command-line tool or import as a Python library +- **Type-safe** — full type hints throughout the public API +- **Well-tested** — pytest suite covering physics and simulation logic + +### Modelling caveats + +This is a deliberately simplified educational model: + +- Motion is **1-D vertical only** (no pitch-over, no horizontal velocity, no orbital insertion). +- **Mass is constant** during flight; propellant burn is not modelled. +- **No atmospheric drag** is applied to trajectories. (`Physics.atmospheric_density` and `Physics.drag_force` are exposed as standalone utilities for users doing their own analyses.) + +For a realistic 6-DOF aerospace simulation, see tools like RocketPy. ## Quick Start @@ -130,9 +141,11 @@ plotter.plot_dashboard(result, filename="dashboard.png") | Long March 5 | 867,000 | 10,600,000 | 480 | 1.25 | | Vega | 137,000 | 2,310,000 | 110 | 1.72 | | Electron | 12,550 | 240,000 | 150 | 1.95 | -| New Shepard | 75,000 | 490,000 | 110 | 0.67 | +| New Shepard † | 75,000 | 490,000 | 110 | 0.67 | | Vulcan Centaur | 546,700 | 11,340,000 | 180 | 2.11 | +† New Shepard's listed first-stage thrust gives T/W < 1 at the masses shown, so the simulator reports immediate landing. The numbers reflect public BE-3 specs; treat the result as a sanity-check signal, not a flight prediction. + ## Physics Model The simulation uses realistic physics including: @@ -149,7 +162,7 @@ The simulation uses realistic physics including: v_esc = √(2GM / (R + h)) ``` -- **Atmospheric Density** (optional): Exponential atmosphere model +- **Atmospheric Density** (utility, *not* applied to trajectories): Exponential atmosphere model exposed via `Physics.atmospheric_density(...)` ``` ρ(h) = ρ₀ × exp(-h / H) ``` diff --git a/src/rocket_sim/__init__.py b/src/rocket_sim/__init__.py index 58dbf4a..972d51c 100644 --- a/src/rocket_sim/__init__.py +++ b/src/rocket_sim/__init__.py @@ -7,11 +7,12 @@ from rocket_sim.config import SimulationConfig from rocket_sim.models import Engine, Rocket, RocketConfig -from rocket_sim.physics import Physics +from rocket_sim.physics import CelestialBody, Physics from rocket_sim.presets import PRESETS, get_preset, list_presets from rocket_sim.simulation import ( RocketSimulation, SimulationResult, + SimulationState, simulate_multiple, simulate_rocket, ) @@ -26,11 +27,13 @@ "RocketConfig", "RocketSimulation", "SimulationResult", + "SimulationState", # Simulation functions "simulate_rocket", "simulate_multiple", # Physics "Physics", + "CelestialBody", # Configuration "SimulationConfig", # Presets diff --git a/src/rocket_sim/cli.py b/src/rocket_sim/cli.py index e54ff44..6c38068 100644 --- a/src/rocket_sim/cli.py +++ b/src/rocket_sim/cli.py @@ -336,7 +336,6 @@ def main(argv: list[str] | None = None) -> int: sim_config = SimulationConfig( dt=args.dt, max_time=args.max_time, - log_level=logging.DEBUG if args.verbose >= 2 else logging.WARNING, ) # Run simulations diff --git a/src/rocket_sim/config.py b/src/rocket_sim/config.py index d0b6e92..df1a654 100644 --- a/src/rocket_sim/config.py +++ b/src/rocket_sim/config.py @@ -8,8 +8,7 @@ from __future__ import annotations import json -import logging -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from typing import Any @@ -23,13 +22,11 @@ class SimulationConfig: dt: Time step for simulation in seconds. max_time: Maximum simulation duration in seconds. detect_escape: Whether to stop simulation on escape velocity. - log_level: Logging verbosity level. """ dt: float = 0.1 max_time: float = 1_000_000.0 detect_escape: bool = True - log_level: int = logging.INFO def __post_init__(self) -> None: """Validate configuration parameters.""" @@ -44,7 +41,6 @@ def to_dict(self) -> dict[str, Any]: "dt": self.dt, "max_time": self.max_time, "detect_escape": self.detect_escape, - "log_level": self.log_level, } @classmethod @@ -54,7 +50,6 @@ def from_dict(cls, data: dict[str, Any]) -> SimulationConfig: dt=float(data.get("dt", 0.1)), max_time=float(data.get("max_time", 1_000_000.0)), detect_escape=bool(data.get("detect_escape", True)), - log_level=int(data.get("log_level", logging.INFO)), ) def save(self, path: Path | str) -> None: @@ -70,46 +65,3 @@ def load(cls, path: Path | str) -> SimulationConfig: with open(path) as f: data = json.load(f) return cls.from_dict(data) - - -@dataclass -class PlotConfig: - """ - Configuration for plot output. - - Attributes: - figsize: Figure size as (width, height) in inches. - dpi: Resolution in dots per inch. - style: Matplotlib style to use. - show_grid: Whether to display grid lines. - show_legend: Whether to display legend. - altitude_unit: Unit for altitude display ('m', 'km', 'mi'). - time_unit: Unit for time display ('s', 'min', 'h'). - """ - - figsize: tuple[float, float] = (12, 8) - dpi: int = 150 - style: str = "seaborn-v0_8-darkgrid" - show_grid: bool = True - show_legend: bool = True - altitude_unit: str = "km" - time_unit: str = "s" - title: str = "Rocket Trajectory Simulation" - - # Unit conversion factors - _altitude_factors: dict[str, float] = field( - default_factory=lambda: {"m": 1.0, "km": 0.001, "mi": 0.000621371}, - repr=False, - ) - _time_factors: dict[str, float] = field( - default_factory=lambda: {"s": 1.0, "min": 1 / 60, "h": 1 / 3600}, - repr=False, - ) - - def get_altitude_factor(self) -> float: - """Get conversion factor for altitude unit.""" - return self._altitude_factors.get(self.altitude_unit, 0.001) - - def get_time_factor(self) -> float: - """Get conversion factor for time unit.""" - return self._time_factors.get(self.time_unit, 1.0) diff --git a/src/rocket_sim/models.py b/src/rocket_sim/models.py index 5130eb9..7df1565 100644 --- a/src/rocket_sim/models.py +++ b/src/rocket_sim/models.py @@ -74,20 +74,15 @@ def from_dict(cls, data: dict[str, float | str]) -> RocketConfig: @dataclass class Engine: """ - Represents a rocket engine. - - Models a simple rocket engine with constant thrust output during - its burn time. + Represents a rocket engine with constant thrust during its burn time. Attributes: thrust: Thrust force in Newtons. burn_time: Duration of thrust in seconds. - specific_impulse: Engine efficiency in seconds (optional). """ thrust: float # Newtons burn_time: float # seconds - specific_impulse: float = 300.0 # seconds (typical for chemical rockets) def __post_init__(self) -> None: """Validate engine parameters.""" @@ -95,8 +90,6 @@ def __post_init__(self) -> None: raise ValueError(f"Thrust cannot be negative: {self.thrust}") if self.burn_time < 0: raise ValueError(f"Burn time cannot be negative: {self.burn_time}") - if self.specific_impulse <= 0: - raise ValueError(f"Specific impulse must be positive: {self.specific_impulse}") @property def total_impulse(self) -> float: @@ -104,7 +97,7 @@ def total_impulse(self) -> float: return self.thrust * self.burn_time def is_burning(self, elapsed_time: float) -> bool: - """Check if engine is still burning at given time.""" + """Check if engine is burning at given time. Returns True for elapsed_time in [0, burn_time).""" return 0 <= elapsed_time < self.burn_time def get_thrust(self, elapsed_time: float) -> float: @@ -125,12 +118,13 @@ class Rocket: """ Represents a rocket with physical state and propulsion. - This class models the rocket's physical properties and tracks its - state (position, velocity) during simulation. + Note: Mass is held constant throughout the simulation; propellant + burn is not modelled. This is the "constant-mass approximation." Attributes: - mass: Rocket mass in kilograms. + mass: Rocket mass in kilograms (treated as constant). engine: Engine instance providing propulsion. + body: Celestial body for gravity calculations. Defaults to Earth. altitude: Current altitude in meters. velocity: Current velocity in m/s (positive = upward). time: Elapsed time since launch in seconds. @@ -138,6 +132,7 @@ class Rocket: mass: float engine: Engine + body: CelestialBody | None = None altitude: float = field(default=0.0, init=False) velocity: float = field(default=0.0, init=False) time: float = field(default=0.0, init=False) @@ -148,18 +143,28 @@ def __post_init__(self) -> None: raise ValueError(f"Mass must be positive: {self.mass}") @classmethod - def from_config(cls, config: RocketConfig) -> Rocket: + def from_config( + cls, + config: RocketConfig, + body: CelestialBody | None = None, + ) -> Rocket: """ Create a Rocket instance from a RocketConfig. Args: config: Configuration parameters for the rocket. + body: Celestial body for gravity. Defaults to Earth. Returns: A new Rocket instance. """ engine = Engine(thrust=config.thrust, burn_time=config.burn_time) - return cls(mass=config.mass, engine=engine) + return cls(mass=config.mass, engine=engine, body=body) + + @property + def _body(self) -> CelestialBody: + """Resolve body to Earth if unset (kept private to avoid changing the dataclass field type).""" + return self.body if self.body is not None else Physics.EARTH def reset(self) -> None: """Reset rocket to initial launch state.""" @@ -167,11 +172,7 @@ def reset(self) -> None: self.velocity = 0.0 self.time = 0.0 - def update( - self, - dt: float, - body: CelestialBody | None = None, - ) -> tuple[float, float]: + def update(self, dt: float) -> tuple[float, float]: """ Update rocket state for a time step. @@ -180,19 +181,19 @@ def update( integration, which conserves energy better than the plain Euler method for orbital trajectories. + Mass is held constant — no propellant burn is modelled. + Args: dt: Time step in seconds. - body: Celestial body for gravity calculation. Defaults to Earth. Returns: Tuple of (altitude, velocity) after the update. """ - # Get current gravity - gravity = Physics.gravity_at_altitude(self.altitude, body) + gravity = Physics.gravity_at_altitude(self.altitude, self._body) - # Calculate acceleration thrust = self.engine.get_thrust(self.time) if thrust > 0: # noqa: SIM108 + # Kept as if/else for readability; thrust and coast acceleration are physically distinct. # During burn: a = (T - mg) / m = T/m - g acceleration = thrust / self.mass - gravity else: @@ -219,15 +220,17 @@ def kinetic_energy(self) -> float: @property def potential_energy(self) -> float: """ - Calculate gravitational potential energy relative to surface. + Calculate gravitational potential energy relative to the surface. - Uses the exact formula accounting for varying gravity with altitude. + Uses the exact integral of GMm/r² (variable gravity), expressed + equivalently as PE = m·g₀·h · R/(R + h), where g₀ and R are the + body's surface gravity and radius. """ - g0 = Physics.EARTH.surface_gravity - r = Physics.EARTH.radius - # PE = mgh * (r / (r + h)) for varying gravity - if self.altitude == 0: + if self.altitude <= 0: return 0.0 + body = self._body + g0 = body.surface_gravity + r = body.radius return self.mass * g0 * self.altitude * (r / (r + self.altitude)) @property @@ -243,4 +246,4 @@ def is_ascending(self) -> bool: @property def is_on_ground(self) -> bool: """Check if rocket is on the ground.""" - return self.altitude == 0 and self.velocity == 0 + return self.altitude <= 0 and self.velocity <= 0 diff --git a/src/rocket_sim/physics.py b/src/rocket_sim/physics.py index 1969ec8..6a05efc 100644 --- a/src/rocket_sim/physics.py +++ b/src/rocket_sim/physics.py @@ -82,10 +82,12 @@ def gravity_at_altitude( ValueError: If altitude is negative. Examples: - >>> Physics.gravity_at_altitude(0) # Surface gravity + >>> Physics.gravity_at_altitude( + ... 0 + ... ) # Surface gravity (from GM/R²; ~9.82, slightly above the conventional 9.80665) 9.819... - >>> Physics.gravity_at_altitude(400_000) # ISS orbit ~400km - 8.676... + >>> Physics.gravity_at_altitude(400_000) # ISS orbit ~400 km + 8.694... """ if altitude < 0: raise ValueError(f"Altitude cannot be negative: {altitude}") @@ -120,9 +122,9 @@ def escape_velocity( Examples: >>> Physics.escape_velocity(0) # From Earth's surface - 11185.7... + 11185.9... >>> Physics.escape_velocity(400_000) # From ISS orbit - 10926.5... + 10850.5... """ if altitude < 0: raise ValueError(f"Altitude cannot be negative: {altitude}") @@ -167,20 +169,25 @@ def orbital_velocity( @staticmethod def atmospheric_density(altitude: float) -> float: """ - Estimate atmospheric density using exponential atmosphere model. + Estimate atmospheric density using an exponential-atmosphere model. This is a simplified model that assumes exponential decay of - atmospheric density with altitude. Accurate for altitudes up to - about 100 km. + atmospheric density with altitude. Reasonable up to about 100 km. + + Note: + This helper is exposed for users doing their own analyses; it + is **not** invoked by the default `RocketSimulation` trajectory + loop. The trajectory simulation is currently atmosphere-free. Args: altitude: Height above surface in meters. Must be >= 0. Returns: - Atmospheric density in kg/m^3. + Atmospheric density in kg/m^3. Returns 0 for altitudes above + 100 km (Karman line). - Note: - Returns 0 for altitudes above 100 km (Karman line). + Raises: + ValueError: If altitude is negative. """ if altitude < 0: raise ValueError(f"Altitude cannot be negative: {altitude}") @@ -207,6 +214,11 @@ def drag_force( Uses the standard drag equation: F_d = 0.5 * rho * v^2 * C_d * A + Note: + Like `atmospheric_density`, this is a standalone utility for + users doing their own analyses. The default `RocketSimulation` + trajectory loop does not apply drag. + Args: velocity: Speed in m/s. altitude: Height above surface in meters. @@ -215,6 +227,9 @@ def drag_force( Returns: Drag force in Newtons (always positive, opposing motion). + + Raises: + ValueError: If altitude is negative (propagated from atmospheric_density). """ rho = Physics.atmospheric_density(altitude) return 0.5 * rho * velocity**2 * drag_coefficient * cross_sectional_area diff --git a/src/rocket_sim/presets.py b/src/rocket_sim/presets.py index e6f19c9..47a4f5c 100644 --- a/src/rocket_sim/presets.py +++ b/src/rocket_sim/presets.py @@ -7,6 +7,8 @@ from __future__ import annotations +from dataclasses import replace + from rocket_sim.models import RocketConfig # Pre-configured rocket specifications based on real-world data @@ -110,15 +112,13 @@ def get_preset(name: str) -> RocketConfig: >>> config.thrust 7607000 """ - # Try exact match first if name in PRESETS: - return PRESETS[name] + return replace(PRESETS[name]) - # Try case-insensitive match name_lower = name.lower() for preset_name, config in PRESETS.items(): if preset_name.lower() == name_lower: - return config + return replace(config) available = ", ".join(PRESETS.keys()) raise KeyError(f"Unknown preset: '{name}'. Available: {available}") diff --git a/src/rocket_sim/simulation.py b/src/rocket_sim/simulation.py index 41dfb8f..518a4fb 100644 --- a/src/rocket_sim/simulation.py +++ b/src/rocket_sim/simulation.py @@ -152,11 +152,9 @@ def __init__( self.sim_config = sim_config or SimulationConfig() self.body = body or Physics.EARTH - # Create rocket instance - self.rocket = Rocket.from_config(rocket_config) - - # Configure logging - logging.basicConfig(level=self.sim_config.log_level) + # Create rocket instance bound to this simulation's body so that + # gravity and energy properties stay consistent. + self.rocket = Rocket.from_config(rocket_config, body=self.body) logger.debug( f"Initialized simulation for {rocket_config.name} " @@ -182,9 +180,9 @@ def step(self, dt: float | None = None) -> SimulationState: dt = self.sim_config.dt old_velocity = self.rocket.velocity - altitude, velocity = self.rocket.update(dt, self.body) + altitude, velocity = self.rocket.update(dt) - acceleration = (velocity - old_velocity) / dt if dt > 0 else 0.0 + acceleration = (velocity - old_velocity) / dt is_burning = self.rocket.engine.is_burning(self.rocket.time) return SimulationState( @@ -307,6 +305,7 @@ def run_generator(self) -> Iterator[SimulationState]: def simulate_rocket( config: RocketConfig, sim_config: SimulationConfig | None = None, + body: CelestialBody | None = None, ) -> SimulationResult: """ Convenience function to run a single rocket simulation. @@ -314,17 +313,19 @@ def simulate_rocket( Args: config: Rocket configuration. sim_config: Optional simulation configuration. + body: Celestial body for gravity. Defaults to Earth. Returns: SimulationResult with trajectory data. """ - sim = RocketSimulation(config, sim_config) + sim = RocketSimulation(config, sim_config, body=body) return sim.run() def simulate_multiple( configs: list[RocketConfig], sim_config: SimulationConfig | None = None, + body: CelestialBody | None = None, ) -> list[SimulationResult]: """ Simulate multiple rockets. @@ -332,12 +333,9 @@ def simulate_multiple( Args: configs: List of rocket configurations. sim_config: Optional simulation configuration (shared). + body: Celestial body for gravity (shared). Defaults to Earth. Returns: List of SimulationResult objects. """ - results = [] - for config in configs: - result = simulate_rocket(config, sim_config) - results.append(result) - return results + return [simulate_rocket(config, sim_config, body=body) for config in configs]