diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index bdc21f21..667ef597 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e .[plot,pybamm,openmdao,dev] + pip install -e .[plot,openmdao,dev] - name: Run tests run: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..e99f039a --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,34 @@ +name: Deploy Docs + +on: + push: + branches: + - main + paths: + - 'docs/**' + - 'mkdocs.yml' + - '.github/workflows/docs.yml' + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-24.04 + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[docs] + + - name: Deploy to GitHub Pages + run: | + mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore index 8390db42..b9b70778 100644 --- a/.gitignore +++ b/.gitignore @@ -10,10 +10,11 @@ venv/ # output folders *_out/ build/ +site/ .antigravitycli/ scratch/ # Test and Coverage .coverage .pytest_cache/ -htmlcov/ \ No newline at end of file +htmlcov/ diff --git a/README.md b/README.md index 9a8a5300..2a3207ca 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,6 @@ PyThrust is an open-source framework for electric propulsion system analysis, co | **3. Propeller Aerodynamic Coefficients** | **4. Hover Efficiency Heatmap** | | ![Propeller Aerodynamic Coefficients](https://raw.githubusercontent.com/Setuav/PyThrust/main/docs/images/propeller_coefficients.png) | ![Hover Efficiency Heatmap](https://raw.githubusercontent.com/Setuav/PyThrust/main/docs/images/efficiency_heatmap.png) | -### 5. PyBaMM Electrochemical Battery Simulation (Dynamic Load) -![PyBaMM Electrochemical Battery Simulation](https://raw.githubusercontent.com/Setuav/PyThrust/main/docs/images/pybamm_mission_results.png) - ## Documentation Please see the [docs/](https://github.com/Setuav/PyThrust/tree/main/docs) folder for design specifications, core mathematical model descriptions, and database details. @@ -25,4 +22,4 @@ PyThrust is licensed under the Apache License, Version 2.0 (the "License"). See ## Copyright -Copyright (c) 2026 Setuav. All rights reserved. \ No newline at end of file +Copyright (c) 2026 Setuav. All rights reserved. diff --git a/docs/api_reference.md b/docs/api_reference.md new file mode 100644 index 00000000..f8ada676 --- /dev/null +++ b/docs/api_reference.md @@ -0,0 +1,132 @@ +# API Reference + +This page summarizes the main public classes and helpers. For implementation details, see the source modules under `pythrust/`. + +## Propulsion Models + +`MotorSpec` defines brushless motor electrical parameters: + +| Field | Unit | Description | +|---|---:|---| +| `kv_rpm_per_v` | RPM/V | Motor speed constant | +| `resistance_ohm` | ohm | Winding resistance | +| `no_load_current_a` | A | Datasheet no-load current | +| `current_max_a` | A | Maximum continuous or configured current limit | +| `torque_constant_kv_ratio` | - | Optional second-order motor model ratio | +| `magnetic_lag_tau` | s | Optional magnetic lag time constant | +| `iron_loss_exponent` | - | Optional no-load current speed scaling exponent | + +Use `get_no_load_current(rpm)` and `get_winding_resistance(current_a)` when evaluating speed-dependent or current-dependent motor behavior. + +## Battery, System, and Propeller Specs + +| Class | Purpose | +|---|---| +| `BatterySpec` | Pack voltage and discharge efficiency | +| `SystemSpec` | Lumped electrical resistance for battery, ESC, wires, and connectors | +| `PropellerSpec` | Propeller geometry passed to the solver | +| `OperatingPoint` | Solved RPM, thrust, torque, power, current, voltage, efficiency, and feasibility state | + +## Propulsion Solver + +`PropulsionSolver` solves the coupled electrical and aerodynamic equilibrium for a single operating condition: + +```python +point = solver.solve_operating_point( + motor=motor, + battery=battery, + system=system, + propeller=propeller, + prop_entry=prop_entry, + rho=1.225, + airspeed_mps=15.0, + throttle=0.7, +) +``` + +`SolverConfig` controls numerical behavior: + +| Field | Default | Description | +|---|---:|---| +| `rpm_min` | `100.0` | Lower RPM bound | +| `rpm_max_margin` | `1.1` | Safety factor on estimated maximum RPM | +| `eps_rpm` | `1e-8` | RPM convergence tolerance | +| `eps_v` | `1e-8` | Voltage residual tolerance | +| `max_iter` | `100` | Maximum root-finder iterations | + +## Propeller Database + +`PropellerDatabase` loads JSON metadata and CSV performance tables: + +```python +from pathlib import Path +from pythrust.propellers import PropellerDatabase + +db = PropellerDatabase() +db.load(Path("data/propellers/apc_202602"), strict=False) +entry = db.get("APC_13x6.5E") +ct, cp = entry.get_coefficients(rpm=5000.0, advance_ratio=0.4) +``` + +Main helpers: + +| Method | Description | +|---|---| +| `load(data_dir, strict=False)` | Load every propeller JSON file in a directory | +| `load_entry(json_path, data_dir=None, strict=False)` | Load one propeller entry | +| `list_propellers()` | Return sorted propeller IDs | +| `get(prop_id)` | Return a `PropellerEntry` by ID | +| `find_by_size(diameter_in, pitch_in, blade_count=2, tolerance=0.5)` | Find the closest size match | +| `get_interpolated_coefficients(...)` | Fetch `Ct` and `Cp` through a size lookup | + +## Motor Database + +`MotorDatabase` loads brushless motor JSON files and converts catalog entries into solver specs: + +```python +from pathlib import Path +from pythrust.motors import MotorDatabase + +db = MotorDatabase() +db.load(Path("data/motors")) +motor_entry = db.get("SunnySky_X2826_KV550") +motor = motor_entry.to_spec() +``` + +Main helpers: + +| Method | Description | +|---|---| +| `load(data_dir)` | Recursively load motor JSON files | +| `load_entry(json_path)` | Load one motor JSON file | +| `list_motors()` | Return sorted motor IDs | +| `get(motor_id)` | Return a `MotorEntry` by ID | +| `search(...)` | Filter by Kv, current, and weight constraints | + +## Calibration + +`PropulsionCalibrator` fits `SystemSpec.resistance_ohm` against manufacturer or thrust-stand data: + +```python +from pythrust.propulsion import PropulsionCalibrator + +calibrator = PropulsionCalibrator(system_bounds=(0.0, 1.0)) +points = calibrator.load_csv("table.csv") +result = calibrator.calibrate( + points, + motor, + battery, + system, + propeller, + prop_entry, +) +system = result.to_system_spec() +``` + +`CalibrationResult` reports fitted resistance, thrust/current RMSE values, thrust `R^2`, convergence status, and quality warnings. + +## OpenMDAO + +`pythrust.openmdao.PropulsionComponent` wraps `PropulsionSolver` as an `ExplicitComponent` for optimization models. + +Inputs include motor parameters, battery voltage, system resistance, propeller diameter, throttle, density, and airspeed. Outputs include RPM, thrust, torque, battery current, battery power, motor current, motor voltage, and feasibility. diff --git a/docs/databases.md b/docs/databases.md index b87d86ec..6a34f97e 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -38,7 +38,7 @@ rpm,speed_mps,advance_ratio,efficiency,thrust_coeff,power_coeff,power_w,torque_n ## 2) Brushless Motor Database (`data/motors/`) -The motor database is a directory of individual JSON files representing fırçasız (brushless) motors. +The motor database is a directory of individual JSON files representing brushless motors. ### Motor Spec format (`*.json`) ```json diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 00000000..6e16133c --- /dev/null +++ b/docs/development.md @@ -0,0 +1,60 @@ +# Development & Testing + +This guide covers local development, tests, examples, and documentation publishing. + +## Local Environment + +```bash +python -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +pip install -e .[plot,openmdao,dev,docs] +``` + +## Run Tests + +```bash +pytest +``` + +The CI workflow runs the test suite on Python 3.10, 3.11, and 3.12. + +## Run Example Workflows + +```bash +PYTHONPATH=. python examples/select_motor.py +PYTHONPATH=. python examples/calibrate_from_datasheet.py +PYTHONPATH=. python examples/optimize_and_plot_propulsion.py +``` + +See [Examples](examples.md) for a user-facing walkthrough of each script. + +Generated plots are written under `docs/images/` and are used by the documentation site. + +## Documentation Site + +PyThrust uses MkDocs Material for a clean, searchable static documentation site. + +Serve locally: + +```bash +mkdocs serve +``` + +Build static HTML: + +```bash +mkdocs build --strict +``` + +Deploy manually to GitHub Pages: + +```bash +mkdocs gh-deploy --force +``` + +## GitHub Pages Publishing + +The `Deploy Docs` workflow publishes the MkDocs site when documentation files, `mkdocs.yml`, or the workflow itself change on `main`. + +In the GitHub repository settings, configure Pages to publish from the `gh-pages` branch after the first successful deploy. diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 00000000..1be010e5 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,129 @@ +# Examples + +PyThrust includes runnable examples that show the main workflows: solving against catalog data, calibrating losses from measurements, and using OpenMDAO for propulsion co-design. + +Run examples from the repository root so relative `data/` and `docs/images/` paths resolve correctly. + +```bash +PYTHONPATH=. python examples/.py +``` + +--- + +## Requirements + +| Example | Extra dependencies | +|---|---| +| `calibrate_from_datasheet.py` | Core PyThrust dependencies | +| `select_motor.py` | `openmdao` | +| `optimize_and_plot_propulsion.py` | `openmdao`, `matplotlib` | + +Install the full example environment: + +```bash +pip install -e .[plot,openmdao] +``` + +--- + +## Datasheet Calibration + +Script: + +```bash +PYTHONPATH=. python examples/calibrate_from_datasheet.py +``` + +This example identifies the lumped system resistance for a motor, propeller, battery, ESC, and wiring setup. + +It uses: + +| Input | Value or source | +|---|---| +| Motor | Datasheet Kv, resistance, no-load current, and current limit | +| Propeller | `APC_13x6.5E` from `data/propellers/apc_202602` | +| Battery | 4S nominal voltage, `14.8 V` | +| Test table | RPM, thrust in grams, and battery current in amps | + +The output reports: + +| Metric | Meaning | +|---|---| +| System resistance | Fitted `SystemSpec.resistance_ohm` | +| Thrust RMSE | Propeller-model thrust error against measured thrust | +| Current RMSE | Battery-current prediction error | +| Thrust R2 | Fit quality for the aerodynamic thrust prediction | +| Per-point table | Predicted vs measured thrust/current for each RPM row | + +See [Motor Calibration](motor_calibration.md) for the calibration model and equations. + +![Calibration results](images/calibration_results.png) + +--- + +## Motor Selection + +Script: + +```bash +PYTHONPATH=. python examples/select_motor.py +``` + +This example combines theoretical co-design with real motor database lookup. + +Workflow: + +1. Load `APC_13x6.5E` propeller data. +2. Use OpenMDAO to find an efficient theoretical motor/propeller/throttle combination for hover. +3. Load the brushless motor database from `data/motors`. +4. Search real motors near the optimized Kv and current requirement. +5. Print the top candidates sorted by winding resistance and weight. + +The optimization target is a hover thrust of `4.903 N`, approximately `500 gf`. + +Typical output includes: + +| Output | Meaning | +|---|---| +| Target Kv | Ideal speed constant from the theoretical optimization | +| Target diameter | Optimized propeller diameter | +| Target hover current | Current at the optimized hover point | +| Minimum hover power | Battery power objective value | +| Top motor matches | Closest catalog motors with Kv, resistance, weight, and current limit | + +See [Component Databases](databases.md) for motor catalog format and query helpers. + +--- + +## Propulsion Optimization and Plotting + +Script: + +```bash +PYTHONPATH=. python examples/optimize_and_plot_propulsion.py +``` + +This example demonstrates OpenMDAO-based propulsion co-design and a parametric Kv sweep. + +It performs three stages: + +1. Run the baseline propulsion model. +2. Optimize motor Kv, propeller diameter, and throttle for a fixed hover thrust. +3. Sweep Kv and re-optimize diameter/throttle at each point. + +The generated plot is saved to: + +```text +docs/images/optimize_and_plot_results.png +``` + +The plot shows: + +| Panel | Shows | +|---|---| +| Power and propeller size vs motor Kv | Hover battery power and optimized propeller diameter | +| Throttle and RPM vs motor Kv | Optimized throttle setting and shaft speed | + +![Propulsion co-design optimization](images/optimize_and_plot_results.png) + +See [Propulsion Solver](usage.md) for the operating-point solver used inside the OpenMDAO component. diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 00000000..2e88f4c7 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,111 @@ +# Getting Started + +This guide walks through installing PyThrust and solving a first propulsion operating point. + +## Requirements + +PyThrust requires Python 3.10 or newer. + +Core dependencies: + +* `numpy` +* `scipy` + +Optional extras: + +* `plot` for visualization examples +* `openmdao` for multidisciplinary design optimization workflows +* `dev` for tests +* `docs` for the MkDocs documentation site + +## Installation + +Clone the repository and install the package in editable mode: + +```bash +git clone https://github.com/Setuav/PyThrust.git +cd PyThrust +python -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +pip install -e . +``` + +For the full development environment: + +```bash +pip install -e .[plot,openmdao,dev,docs] +``` + +## First Solve + +The core workflow is: + +1. Load a propeller aerodynamic dataset. +2. Define motor, battery, system, and propeller specifications. +3. Solve the operating point for an airspeed and throttle. + +```python +from pathlib import Path + +from pythrust.propellers import PropellerDatabase +from pythrust.propulsion import ( + BatterySpec, + MotorSpec, + PropellerSpec, + PropulsionSolver, + SystemSpec, +) + +prop_db = PropellerDatabase() +prop_db.load(Path("data/propellers/apc_202602"), strict=False) +prop_entry = prop_db.get("APC_13x6.5E") + +motor = MotorSpec( + kv_rpm_per_v=860.0, + resistance_ohm=0.0258, + no_load_current_a=1.3, + current_max_a=65.0, +) +battery = BatterySpec(voltage_v=14.8) +system = SystemSpec(resistance_ohm=0.05) +propeller = PropellerSpec(diameter_m=0.3302, blade_count=2) + +solver = PropulsionSolver() +point = solver.solve_operating_point( + motor=motor, + battery=battery, + system=system, + propeller=propeller, + prop_entry=prop_entry, + rho=1.225, + airspeed_mps=15.0, + throttle=0.7, +) + +print(point.rpm) +print(point.thrust_n) +print(point.motor_current_a) +print(point.is_feasible) +``` + +## Run the Examples + +```bash +PYTHONPATH=. python examples/select_motor.py +PYTHONPATH=. python examples/calibrate_from_datasheet.py +PYTHONPATH=. python examples/optimize_and_plot_propulsion.py +``` + +The plotting and OpenMDAO examples require the optional dependencies shown above. + +See [Examples](examples.md) for the purpose, inputs, and outputs of each script. + +## Build the Documentation Locally + +```bash +pip install -e .[docs] +mkdocs serve +``` + +Then open the local MkDocs URL printed in the terminal. diff --git a/docs/images/setuav_logo.png b/docs/images/setuav_logo.png new file mode 100644 index 00000000..93908326 Binary files /dev/null and b/docs/images/setuav_logo.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..0a15cf6a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,40 @@ +# PyThrust + +![PyThrust Banner](images/PyThrust_banner.png) + +Welcome to the official documentation for **PyThrust** - an open-source Python framework for electric propulsion system analysis, co-design, and parameter optimization in UAV applications. + +PyThrust combines empirical propeller data, brushless motor models, battery/system loss modeling, and OpenMDAO integration so UAV designers can move from theoretical propulsion sizing to real component choices with traceable calculations. + +--- + +## Why PyThrust? + +Electric UAV propulsion design usually crosses several domains: aerodynamics, motor electrical behavior, battery loading, component catalogs, and mission constraints. PyThrust keeps those pieces in one workflow: + +* **Coupled operating-point solver:** Solve equilibrium RPM, thrust, torque, current, power, and efficiency for a motor, propeller, battery, and flight condition. +* **Catalog-backed selection:** Query real brushless motor and propeller datasets instead of optimizing against abstract components only. +* **Calibration tools:** Fit lumped system resistance from test-stand data to account for ESC, battery, wiring, and connector losses. +* **Optimization-ready components:** Use the propulsion solver inside OpenMDAO for multidisciplinary design optimization studies. +* **Optimization-ready examples:** Generate plots for design sweeps, calibration quality, propeller coefficients, and hover efficiency maps. + +--- + +## Design and Analysis Visualization + +| Propulsion Co-Design Optimization | Propulsion Calibration & Auto-Tuning | +| :---: | :---: | +| ![Propulsion co-design optimization](images/optimize_and_plot_results.png) | ![Propulsion calibration and auto-tuning results](images/calibration_results.png) | +| **Propeller Aerodynamic Coefficients** | **Hover Efficiency Heatmap** | +| ![Propeller aerodynamic coefficients](images/propeller_coefficients.png) | ![Hover efficiency heatmap](images/efficiency_heatmap.png) | + +## Key Documentation Sections + +* [**Getting Started**](getting_started.md): Installation, optional extras, and a first operating-point solve. +* [**Propulsion Solver**](usage.md): Solver configuration, feasibility rules, and usage examples. +* [**Motor Calibration**](motor_calibration.md): Fit system resistance from manufacturer or thrust-stand data. +* [**Examples**](examples.md): Runnable scripts for calibration, motor selection, and OpenMDAO optimization. +* [**API Reference**](api_reference.md): Main classes, database loaders, calibration objects, and OpenMDAO wrapper. +* [**Mathematical Model**](theory.md): Propeller, motor, electrical loss, and coupled equilibrium equations. +* [**Component Databases**](databases.md): Propeller CSV/JSON and motor catalog formats. +* [**Development & Testing**](development.md): Local setup, tests, examples, and documentation build commands. diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js new file mode 100644 index 00000000..d883c521 --- /dev/null +++ b/docs/javascripts/mathjax.js @@ -0,0 +1,11 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"], ["$$", "$$"]], + processEscapes: true, + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex", + }, +}; diff --git a/docs/motor_calibration.md b/docs/motor_calibration.md index 88415faf..6d66da5c 100644 --- a/docs/motor_calibration.md +++ b/docs/motor_calibration.md @@ -10,38 +10,130 @@ The `PropulsionCalibrator` identifies a single lumped parameter, `system.resista ## Physical Loss Model -### Voltage Balance +### Symbols -At a given `throttle` and battery voltage $V_{\text{bat}}$, the ideal average voltage applied by PWM switching is $V_{\text{applied}} = \text{throttle} \times V_{\text{bat}}$. +| Symbol | Meaning | +|---|---| +| $K_v$ | Motor speed constant from the datasheet | +| $R_m$ | Motor winding resistance from the datasheet | +| $I_0$ | Motor no-load current from the datasheet | +| $R_{\text{system}}$ | Lumped resistance being calibrated | +| $V_{\text{bat}}$ | Battery voltage during the test | +| $V_{\text{applied}}$ | Average voltage commanded by throttle | +| $V_{\text{back}}$ | Motor back-EMF voltage | +| $V_m$ | Motor terminal voltage | +| $I_{\text{motor}}$ | Motor winding current predicted from propeller torque | +| $I_{\text{bat,pred}}$ | Predicted battery current | +| $\tau$ | Propeller shaft torque from the aerodynamic database | -The voltage actually reaching the motor terminals is reduced by transmission voltage drops: -$$V_{\text{motor}} = V_{\text{applied}} - I_{\text{motor}} R_{\text{system}}$$ +### Step 1: Applied Voltage -Equating this to the motor's internal back-EMF voltage balance ($V_{\text{motor}} = V_{\text{back}} + I_{\text{motor}} R_m$): -$$\text{throttle} \times V_{\text{bat}} = V_{\text{back}} + I_{\text{motor}} (R_m + R_{\text{system}})$$ +At a given throttle and battery voltage, the ideal average voltage applied by PWM switching is: -where -$$V_{\text{back}} = \frac{\text{RPM}}{K_v}, \qquad I_{\text{motor}} = \frac{\tau}{K_t} + I_0, \qquad K_t = \frac{60}{2\pi K_v}$$ +$$ +V_{\text{applied}} = +\text{throttle} \cdot V_{\text{bat}} +$$ -$\tau$ is the propeller shaft torque determined from the aerodynamic database at the measured RPM. +Some of this voltage is lost before it reaches the motor because the battery, ESC, wires, and connectors all have finite resistance. -### Power Balance & Battery Current +### Step 2: Motor State at the Measured RPM -The electrical power drawn from the battery is the sum of motor power and transmission conduction losses ($I_{\text{motor}}^2 R_{\text{system}}$): -$$P_{\text{battery}} = V_{\text{motor}} I_{\text{motor}} + I_{\text{motor}}^2 R_{\text{system}} = V_{\text{back}} I_{\text{motor}} + I_{\text{motor}}^2 (R_m + R_{\text{system}})$$ +For each measured RPM, PyThrust first evaluates the propeller torque from the propeller database. That torque determines the motor current needed to spin the propeller: -Thus, the predicted battery DC current is: -$$I_{\text{bat\_pred}}(R_{\text{system}}) = \frac{V_m I_{\text{motor}} + I_{\text{motor}}^2 R_{\text{system}}}{V_{\text{bat}}}$$ +$$ +K_t = \frac{60}{2 \pi K_v} +$$ -where $V_m = V_{\text{back}} + I_{\text{motor}} R_m$ is the motor terminal voltage. +$$ +I_{\text{motor}} = +\frac{\tau}{K_t} + I_0 +$$ + +The motor back-EMF voltage is: + +$$ +V_{\text{back}} = +\frac{\text{RPM}}{K_v} +$$ + +The motor terminal voltage is then: + +$$ +V_m = +V_{\text{back}} + I_{\text{motor}} R_m +$$ + +### Step 3: Add System Losses + +The calibrated resistance is added outside the motor winding resistance: + +$$ +V_{\text{applied}} = +V_{\text{back}} ++ I_{\text{motor}} R_m ++ I_{\text{motor}} R_{\text{system}} +$$ + +Equivalently: + +$$ +V_{\text{applied}} = +V_{\text{back}} ++ I_{\text{motor}} (R_m + R_{\text{system}}) +$$ + +This is the voltage-balance view of the same physical loss model. + +### Step 4: Predict Battery Current + +The battery must supply both motor electrical power and the extra conduction loss in the system resistance: + +$$ +P_{\text{battery}} = +V_m I_{\text{motor}} ++ I_{\text{motor}}^2 R_{\text{system}} +$$ + +The predicted battery current for a candidate resistance value is: + +$$ +I_{\text{bat,pred}}(R_{\text{system}}) = +\frac{ + V_m I_{\text{motor}} + + I_{\text{motor}}^2 R_{\text{system}} +}{ + V_{\text{bat}} +} +$$ + +Here, $R_{\text{system}}$ is the only unknown parameter being fitted. --- ## Identification Procedure -Given **N** test points $\{(RPM_i, T_i, I_i)\}$ from a thrust stand, the calibrator solves the least-squares problem: - -$$\hat{R}_{\text{system}} = \arg\min_{R \in [0.0,\, 1.0]} \sum_{i=1}^N \left[ \frac{I^{\text{pred}}_i(R) - I^{\text{meas}}_i}{I_{\max}} \right]^2$$ +Given **N** test points from a thrust stand, each point contains: + +$$ +(\text{RPM}_i,\ T_i,\ I_{\text{bat,meas},i}) +$$ + +The calibrator chooses the system resistance that minimizes the normalized current error: + +$$ +\hat{R}_{\text{system}} = +\arg\min_{R \in [0.0,\, 1.0]} +\sum_{i=1}^{N} +\left( + \frac{ + I_{\text{bat,pred},i}(R) + - I_{\text{bat,meas},i} + }{ + I_{\max} + } +\right)^2 +$$ This is a linear optimization problem in $R_{\text{system}}$ and is solved using `scipy.optimize.least_squares` with bound constraints to prevent non-physical negative resistance. diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 00000000..7cb9649a --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,57 @@ +/* PyThrust documentation accents, aligned with the Setuav MkDocs Material style. */ + +:root { + --pythrust-blue: #1565c0; + --pythrust-green: #2e7d32; + --pythrust-orange: #ef6c00; +} + +.md-typeset img { + border-radius: 6px; +} + +.md-typeset table:not([class]) { + font-size: 0.78rem; +} + +.md-typeset h1 { + font-weight: 700; +} + +[data-md-color-scheme="default"] .md-typeset a { + color: var(--pythrust-blue); +} + +[data-md-color-scheme="slate"] .md-typeset a { + color: #90caf9; +} + +[data-md-color-scheme="default"] .node.propStyle rect { + fill: #e3f2fd !important; + stroke: #1565c0 !important; +} + +[data-md-color-scheme="default"] .node.motorStyle rect { + fill: #f1f8e9 !important; + stroke: #558b2f !important; +} + +[data-md-color-scheme="default"] .node.batteryStyle rect { + fill: #fff3e0 !important; + stroke: #ef6c00 !important; +} + +[data-md-color-scheme="slate"] .node.propStyle rect { + fill: #1e3a8a !important; + stroke: #93c5fd !important; +} + +[data-md-color-scheme="slate"] .node.motorStyle rect { + fill: #14532d !important; + stroke: #86efac !important; +} + +[data-md-color-scheme="slate"] .node.batteryStyle rect { + fill: #7c2d12 !important; + stroke: #fdba74 !important; +} diff --git a/docs/usage.md b/docs/usage.md index f5617196..7e7383f1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,54 +1,252 @@ # Propulsion Solver User & API Guide -This guide describes how to configure, run, and analyze the equilibrium RPM propulsion solver for single operating points and sweeps. +This guide describes how PyThrust solves a motor-propeller operating point and how to interpret the result. -## 1) Problem Statement +The solver answers one core question: -For a given throttle setting, flight airspeed, and propeller/motor specification, PyThrust solves for the equilibrium shaft speed (RPM) such that the motor's internal terminal voltage matches the applied throttle voltage: +> Given a motor, propeller, battery, throttle, and airspeed, what shaft RPM makes the electrical motor model and propeller aerodynamic load agree? +--- + +## Solver Inputs + +The operating-point solver combines five inputs: + +| Input | Class or value | Purpose | +|---|---|---| +| Motor | `MotorSpec` | Kv, winding resistance, no-load current, current limit, optional higher-order loss terms | +| Battery | `BatterySpec` | Pack voltage and discharge efficiency | +| System | `SystemSpec` | Lumped resistance for ESC, battery internal resistance, wiring, and connectors | +| Propeller | `PropellerSpec` + `PropellerEntry` | Geometry plus empirical aerodynamic coefficients | +| Flight condition | `rho`, `airspeed_mps`, `throttle` | Air density, freestream speed, and commanded voltage fraction | + +--- + +## Symbols + +| Symbol | Meaning | +|---|---| +| $\text{RPM}$ | Propeller shaft speed being solved | +| $n$ | Shaft speed in revolutions per second | +| $V$ | Flight airspeed | +| $D$ | Propeller diameter | +| $J$ | Propeller advance ratio | +| $C_t$, $C_p$ | Propeller thrust and power coefficients | +| $T$ | Thrust | +| $Q$ | Propeller shaft torque | +| $K_v$ | Motor speed constant | +| $K_t$ | Motor torque constant | +| $I$ | Motor winding current | +| $I_0$ | No-load current | +| $R_m$ | Motor winding resistance | +| $R_{\text{system}}$ | Lumped system resistance | +| $V_{\text{back}}$ | Motor back-EMF voltage | +| $V_m$ | Motor terminal voltage | +| $V_{\text{pack}}$ | Battery pack voltage | + +--- + +## What the Solver Does + +### Step 1: Evaluate the propeller at a candidate RPM + +For each candidate RPM, PyThrust converts shaft speed to revolutions per second: + +$$ +n = \frac{\text{RPM}}{60} $$ -g(\text{RPM}) = V_{\text{back}}(\text{RPM}) + I(\text{RPM}) R + I(\text{RPM}) R_{\text{system}} - \text{throttle} \times V_{\text{pack}} = 0 + +Then it calculates the propeller advance ratio: + $$ +J = \frac{V}{nD} +$$ + +The propeller database returns empirical coefficients at that RPM and advance ratio: + +$$ +C_t = C_t(\text{RPM}, J) +$$ + +$$ +C_p = C_p(\text{RPM}, J) +$$ + +From those coefficients, PyThrust computes thrust, torque, and shaft power: -Once the root of $g(\text{RPM}) = 0$ is found, the solver evaluates the complete aerodynamic and electrical state (thrust, torque, currents, powers, efficiency). +$$ +T = C_t \rho n^2 D^4 +$$ + +$$ +Q = \frac{C_p \rho n^2 D^5}{2\pi} +$$ + +$$ +P_{\text{shaft}} = C_p \rho n^3 D^5 +$$ + +### Step 2: Evaluate the motor state + +By default, PyThrust uses a first-order brushless DC motor model. The torque constant is: + +$$ +K_t = +\frac{30}{\pi K_v} +$$ + +The motor current needed to drive the propeller torque is: + +$$ +I = +\frac{Q}{K_t} + I_0 +$$ + +The back-EMF voltage is: + +$$ +V_{\text{back}} = +\frac{\text{RPM}}{K_v} +$$ + +The motor terminal voltage is: + +$$ +V_m = +V_{\text{back}} + I R_m +$$ + +### Drela / QPROP Motor Model Notes + +`MotorSpec` also includes optional parameters inspired by Mark Drela's QPROP motor models: + +| Field | Effect | +|---|---| +| `torque_constant_kv_ratio` | Adjusts the relation between speed constant and torque constant | +| `magnetic_lag_tau` | Adds magnetic lag to the back-EMF relation | +| `no_load_current_linear` | Adds a linear speed term to no-load current | +| `no_load_current_quadratic` | Adds a quadratic speed term to no-load current | +| `resistance_quadratic` | Adds current-dependent winding resistance | +| `iron_loss_exponent` | Scales no-load current with RPM using a power law | + +When these fields are left at their defaults, the model reduces to the simpler first-order motor equations above. See [Mathematical Model](theory.md) for the full equations and Drela references. + +--- + +## Equilibrium RPM + +The throttle command defines the average voltage available from the battery: + +$$ +V_{\text{applied}} = +\text{throttle} \cdot V_{\text{pack}} +$$ + +The solver searches for the RPM where the motor voltage demand plus system voltage drop equals the applied voltage: + +$$ +g(\text{RPM}) = +V_m(\text{RPM}) ++ I(\text{RPM}) R_{\text{system}} +- V_{\text{applied}} +$$ + +The equilibrium point is: + +$$ +g(\text{RPM}) = 0 +$$ + +PyThrust solves this scalar root-finding problem with Brent's method through `scipy.optimize.root_scalar`. + +--- + +## RPM Bracket + +Before solving, PyThrust builds a search interval: + +| Bound | How it is chosen | +|---|---| +| Lower RPM | At least `SolverConfig.rpm_min`; raised if airspeed and propeller `J` range require a higher RPM | +| Upper RPM | Estimated from `motor.kv_rpm_per_v * battery.voltage_v * throttle`, then expanded by `rpm_max_margin` | + +If the residual does not change sign inside the bracket, the point is returned as infeasible with reason `no_bracket`. --- -## 2) Solver Configuration (`SolverConfig`) +## Solver Configuration The numerical behavior of the root finder is controlled by `SolverConfig`: | Parameter | Type | Default | Description | -|---|---|---|---| +|---|---|---:|---| | `rpm_min` | `float` | `100.0` | Lower bound limit for RPM | -| `rpm_max_margin` | `float` | `1.1` | Safety scaling factor applied to calculated max RPM upper bound | -| `eps_rpm` | `float` | `1e-8` | Convergence tolerance for shaft speed (RPM) | -| `eps_v` | `float` | `1e-8` | Convergence tolerance for terminal voltage residuals | -| `max_iter` | `int` | `100` | Maximum iterations permitted for root finder | +| `rpm_max_margin` | `float` | `1.1` | Safety scaling factor applied to the upper RPM estimate | +| `eps_rpm` | `float` | `1e-8` | Convergence tolerance for shaft speed | +| `eps_v` | `float` | `1e-8` | Voltage residual tolerance | +| `max_iter` | `int` | `100` | Maximum root-finder iterations | --- -## 3) Example Usage +## Result Fields + +`solve_operating_point(...)` returns an `OperatingPoint`: + +| Field | Meaning | +|---|---| +| `rpm` | Solved shaft speed | +| `advance_ratio` | Propeller advance ratio at the solved point | +| `ct`, `cp` | Interpolated thrust and power coefficients | +| `thrust_n` | Thrust force | +| `torque_nm` | Propeller shaft torque | +| `shaft_power_w` | Mechanical shaft power | +| `motor_power_w` | Electrical power at motor terminals | +| `battery_power_w` | Battery-side power including system losses | +| `motor_current_a` | Motor winding current | +| `motor_voltage_v` | Motor terminal voltage | +| `propeller_efficiency` | Propulsive efficiency based on thrust power | +| `motor_efficiency` | Shaft power divided by motor electrical power | +| `system_efficiency` | Thrust power divided by battery power | +| `is_feasible` | Whether the point passed feasibility checks | +| `infeasible_reason` | Reason string when `is_feasible` is false | + +--- + +## Feasibility Rules + +An operating point is marked as infeasible when one of these checks fails: + +| Reason | Meaning | +|---|---| +| `throttle<=0` | No positive throttle command | +| `no_bracket` | The RPM search interval does not contain a valid voltage-balance root | +| `no_convergence` | The root finder did not converge | +| `current_limit` | `motor_current_a` exceeds `current_max_a` | +| `invalid_coefficients` | Propeller coefficient lookup produced non-physical values | +| `invalid_efficiency` | Computed efficiency is outside the physically expected range | + +--- + +## Example Usage Here is a complete example showing how to load a propeller dataset, define specifications, and solve for an operating point: ```python from pathlib import Path + from pythrust.propellers import PropellerDatabase from pythrust.propulsion import ( BatterySpec, MotorSpec, PropellerSpec, - SystemSpec, PropulsionSolver, + SystemSpec, ) -# 1. Load propeller aerodynamic dataset db = PropellerDatabase() db.load(Path("data/propellers/apc_202602"), strict=False) prop_entry = db.get("APC_13x6.5E") -# 2. Define component specifications motor = MotorSpec( kv_rpm_per_v=860.0, resistance_ohm=0.0258, @@ -58,13 +256,12 @@ motor = MotorSpec( battery = BatterySpec( voltage_v=14.8, - discharge_efficiency=1.0 + discharge_efficiency=1.0, ) system = SystemSpec(resistance_ohm=0.05) propeller = PropellerSpec(diameter_m=0.3302, blade_count=2) -# 3. Solve operating point solver = PropulsionSolver() point = solver.solve_operating_point( motor=motor, @@ -77,35 +274,10 @@ point = solver.solve_operating_point( throttle=0.7, ) -# 4. View results -print(f"RPM : {point.rpm:.1f}") -print(f"Thrust : {point.thrust_n:.2f} N") -print(f"Battery Current : {point.battery_power_w / battery.voltage_v:.2f} A") -print(f"Is Feasible : {point.is_feasible}") -``` - ---- - -## 4) Feasibility Rules - -An operating point is marked as infeasible (`point.is_feasible = False`) if: -- `motor_current_a > current_max_a` (motor current limit exceeded) -- `ct < 0` or `cp < 0` or `advance_ratio < 0` (aerodynamic coefficients out of physical range) -- No valid RPM bracket with a voltage sign change is found. - ---- - -## 5) PyBaMM Electrochemical Battery Simulation Example - -You can run a dynamic flight mission simulation that integrates the **Single Particle Model (SPM)** lithium-ion battery solver from **PyBaMM** with the propulsion model. The simulation will calculate dynamic cell voltage, state of charge (SoC) via electrochemical diffusion, and system thrust. - -To run the PyBaMM example: -```bash -PYTHONPATH=. .venv/bin/python examples/simulate_pybamm_mission.py +print(f"RPM : {point.rpm:.1f}") +print(f"Thrust : {point.thrust_n:.2f} N") +print(f"Motor Current : {point.motor_current_a:.2f} A") +print(f"Battery Power : {point.battery_power_w:.1f} W") +print(f"Feasible : {point.is_feasible}") +print(f"Reason : {point.infeasible_reason}") ``` -This generates a detailed plot showing: -- Throttle profile and terminal voltage under dynamic loads (capturing electrochemical voltage sag and relaxation recovery). -- Non-linear State of Charge (SoC %) based on discharge capacity. -- Motor current draw and produced thrust. - -The plot is saved to [pybamm_mission_results.png](file:///home/huseyin/setuav/PyThrust/docs/images/pybamm_mission_results.png). diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..da791798 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,76 @@ +site_name: PyThrust +site_description: Electric propulsion system co-design, analysis, and optimization framework for UAVs +site_author: Hüseyin Karakaya +repo_url: https://github.com/Setuav/PyThrust +repo_name: Setuav/PyThrust +edit_uri: edit/main/docs/ + +theme: + name: material + logo: images/setuav_logo.png + favicon: images/setuav_logo.png + font: + text: Ubuntu + code: Ubuntu Mono + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: white + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + +extra_css: + - stylesheets/extra.css + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.tabbed: + alternate_style: true + - pymdownx.arithmatex: + generic: true + +extra_javascript: + - javascripts/mathjax.js + - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js + +nav: + - User Guide: + - Home: index.md + - Getting Started: getting_started.md + - Propulsion Solver: usage.md + - Motor Calibration: motor_calibration.md + - Examples: examples.md + - Reference: + - API Reference: api_reference.md + - Mathematical Model: theory.md + - Component Databases: databases.md + - Developer Guide: + - Development & Testing: development.md diff --git a/pyproject.toml b/pyproject.toml index 95a70c75..8a70f1c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,15 +30,16 @@ dependencies = [ plot = [ "matplotlib>=3.4", ] -pybamm = [ - "pybamm>=25.0", -] openmdao = [ "openmdao>=3.20.0", ] dev = [ "pytest>=7.0", ] +docs = [ + "mkdocs-material>=9.5", + "pymdown-extensions>=10.0", +] [tool.setuptools.dynamic] version = {attr = "pythrust.__version__"} diff --git a/requirements.txt b/requirements.txt index c8f75835..8d6589d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,9 +5,6 @@ scipy>=1.7 # Optional Dependencies (For visualization and plotting) matplotlib>=3.4 -# Optional Dependencies (For electrochemical battery simulation examples) -pybamm>=25.0 - # Optional Dependencies (For Multidisciplinary Design Optimization - MDO examples) openmdao>=3.20.0