Propulsion Solver User & API Guide
This guide describes how PyThrust solves a motor-propeller operating point and how to interpret the result.
The solver answers one core question:
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:
Then it calculates the propeller advance ratio:
The propeller database returns empirical coefficients at that RPM and advance ratio:
From those coefficients, PyThrust computes thrust, torque, and shaft power:
Step 2: Evaluate the motor state
By default, PyThrust uses a first-order brushless DC motor model. The torque constant is:
The motor current needed to drive the propeller torque is:
The back-EMF voltage is:
The motor terminal voltage is:
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 for the full equations and Drela references.
Equilibrium RPM
The throttle command defines the average voltage available from the battery:
The solver searches for the RPM where the motor voltage demand plus system voltage drop equals the applied voltage:
The equilibrium point is:
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.
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 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 |
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:
from pathlib import Path
from pythrust.propellers import PropellerDatabase
from pythrust.propulsion import (
BatterySpec,
MotorSpec,
PropellerSpec,
PropulsionSolver,
SystemSpec,
)
db = PropellerDatabase()
db.load(Path("data/propellers/apc_202602"), strict=False)
prop_entry = 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,
discharge_efficiency=1.0,
)
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(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}")