Steady-state outputs
A single-point simulation and a sweep share one output schema. A completed result carries two siblings:
result— the computed outputs, keyed per-rotor and by aggregate.display_labels— a{snake_key: human_string}map: the source of truth for a human-readable column header (thrust_n→"Thrust (N)").
Every per-rotor and aggregate key is canonical snake_case. A sweep reuses the
exact same per-point shape, so everything on this page applies to both.
Result shape
result is a dict keyed by:
| Key | Contents |
|---|---|
"1", "2", … | One entry per rotor group, in submit order. Per-rotor performance, aero, ESC, and thermal fields. |
"All" | Vehicle-level roll-ups summed or aggregated across every rotor. |
"Battery" | Pack-level state and per-cell arrays. |
sim = client.simulations.retrieve("sim_2c5tQ...")
r = sim["result"]
labels = sim["display_labels"]
r["1"]["thrust_n"] # 9.81 — per-rotor thrust, newtons
r["All"]["total_thrust_n"] # 39.24 — vehicle total across 4 rotors
labels["thrust_n"] # "Thrust (N)" — the display header for that key
Reading display_labels
display_labels is one map for the whole result. Look a key up to render it;
fall back to the raw key when it is absent:
for key, value in r["1"].items():
header = labels.get(key, key)
print(f"{header}: {value}")
The map covers every per-rotor and "All" key. It does not cover the
"Battery" entry — those keys are already human strings (see below).
Per-rotor fields (result["1"], result["2"], …)
Identification
| Key | Label | Meaning |
|---|---|---|
propeller | Propeller | Propeller name. |
motor | Motor | Motor name (omitted when empty). |
Performance
| Key | Label | Units |
|---|---|---|
throttle_pct | Throttle (%) | % |
rpm | RPM | rev/min |
thrust_n | Thrust (N) | N (thrust carries the same value) |
static_torque | Static Torque | N·m |
dynamic_torque | Dynamic Torque | N·m (0 in steady state) |
total_torque | Total Torque | N·m |
power | Power | W (shaft mechanical) |
motor_voltage_v | Motor Voltage (V) | V |
battery_voltage_v | Battery Voltage (V) | V |
current_a | Current (A) | A |
shaft_power_w | Shaft Power (W) | W |
mechanical_power_w | Mechanical Power (W) | W |
electrical_power_w | Electrical Power (W) | W |
g_per_w | g/W | g/W (thrust per electrical watt) |
efficiency_pct | Efficiency (%) | % |
weight_g | Weight (g) | g (motor + propeller) |
Aerodynamics
| Key | Label | Units |
|---|---|---|
j | J | Advance ratio (dimensionless) |
ct | Ct | Thrust coefficient (dimensionless) |
cp | Cp | Power coefficient (dimensionless) |
pe | Pe | Ideal (induced) power, W |
mach | Mach | Tip Mach number |
reyn | Reyn | Blade Reynolds number |
thr_per_pwr | THR/PWR | g/W |
ve | Ve | Exit velocity, m/s (exit_velocity is the same value) |
h_force_n | H-force (N) | In-plane hub force, N (present only for an active oblique flight condition — nonzero edgewise inflow) |
inflow_validity | inflow_validity | "ok" or "vrs_band", categorical (same presence rule as h_force_n) |
f_vertical_n | Vert force (N) | Ground-frame vertical force, N (ground mode only — inflow_mode="components" has no tilt angle to resolve a ground frame) |
f_horizontal_n | Horiz force (N) | Ground-frame horizontal force, N (ground mode only) |
ESC
| Key | Label | Units |
|---|---|---|
esc_voltage_drop_v | ESC Voltage Drop (V) | V |
esc_power_loss_w | ESC Power Loss (W) | W |
esc_efficiency | ESC Efficiency | Fraction (0–1) |
bus_current_a | Bus Current (A) | A |
motor_voltage_after_esc_v | Motor Voltage After ESC (V) | V |
Thermal
Present when the coupled electromagnetic + thermal solve runs.
| Key | Label | Units |
|---|---|---|
t_w_degc | T_w (degC) | Winding temperature, °C |
t_mag_degc | T_mag (degC) | Magnet temperature, °C |
t_ambient_degc | T_ambient (degC) | Ambient temperature, °C |
p_motor_w | P_motor (W) | Motor dissipation, W |
r_th_k_per_w | R_th (K/W) | Effective thermal resistance, K/W |
motor_cooling_velocity_m_s | Motor cooling velocity (m/s) | Cooling-air velocity, m/s |
thermal_converged | Thermal converged | Boolean |
thermal_iterations | Thermal iterations | Count |
irreversible_magnet_warning | Irreversible magnet warning | Boolean |
Solver-quality flags
frac_converged, compressibility_warning, and frac_above_Mdd stay top-level
on each rotor entry: they report per-rotor convergence and compressibility state.
Per-rotor solver diagnostics and build metadata are relocated under a neutral
result["1"]["diagnostics"] sub-object, off the physical surface.
Aggregate fields (result["All"])
| Key | Label | Units |
|---|---|---|
total_thrust_n | Total Thrust (N) | N |
total_electrical_power_w | Total Electrical Power (W) | W |
total_shaft_power_w | Total Shaft Power (W) | W |
total_mechanical_power_w | Total Mechanical Power (W) | W |
total_current_a | Total Current (A) | A |
total_bus_current_a | Total Bus Current (A) | A |
total_g_per_w | Total g/W | g/W |
total_efficiency_pct | Total Efficiency (%) | % |
total_weight_g | Total Weight (g) | g |
total_esc_power_loss_w | Total ESC Power Loss (W) | W |
f_vertical_total_n | Total F_vertical (N) | N (count-weighted vehicle total; ground mode only) |
f_horizontal_total_n | Total F_horizontal (N) | N (count-weighted vehicle total; ground mode only) |
Thermal roll-ups (max_t_w_degc, max_t_mag_degc, max_t_core_degc,
max_t_case_degc, total_motor_dissipation_w, total_battery_dissipation_w,
pack_thermal_coupling_warning, …) appear when the thermal solve runs. The
"All" build metadata is relocated under result["All"]["diagnostics"].
Battery entry (result["Battery"])
The "Battery" entry keeps human-string keys and is not covered by
display_labels — read these keys directly:
| Key | Units |
|---|---|
Total Voltage (V) | V |
Current Draw (A) | A |
Power Draw (W) | W |
Cell voltages (V) | List, V per cell |
Charge levels (%) | List, SOC % per cell |
Internal Resistance (Ohm) | Ω |
Configuration | e.g. "4S1P" |
Remaining Capacity (mAh) | mAh |
Est. Time Remaining (s) | s |
Per-cell thermal arrays (T_core, T_case, R_cell, …) ride the same entry
when the thermal solve runs.
Worked example
Runs a single point and a sweep, then prints each key next to its
display_labels header:
"""Read a steady-state result: single-point AND sweep share one snake_case schema.
Run it:
export THRUSTLAB_API_KEY=key_... # never hard-code the key
python examples/outputs/read_steady_state.py
A completed single-point simulation carries two siblings:
* result — a dict keyed per-rotor ("1", "2", ...) plus the "All"
and "Battery" aggregates. Every per-rotor and "All" key
is canonical snake_case (e.g. thrust_n, total_current_a).
* display_labels — {snake_key: human_string}, the single source of truth for
a human-readable column header (thrust_n -> "Thrust (N)").
A sweep reuses the EXACT same per-point shape: each point's `rotors` mirrors a
single-point `result`, and the same display_labels vocabulary applies. Swap in
your own component IDs, or resolve them by name with client.components.find(...).
"""
from thrustlab import Client
client = Client() # reads $THRUSTLAB_API_KEY from the environment
def print_group(name: str, group: dict, labels: dict) -> None:
"""Print each key -> value alongside its human display label.
Scalar keys print as `snake_key Human Label value`. Nested blocks
(the relocated `diagnostics` sub-object) print their key set only.
"""
print(f"\n[{name}]")
for key, value in group.items():
label = labels.get(key, key) # "Battery" keys are already human strings
if isinstance(value, (dict, list)):
kind = "dict" if isinstance(value, dict) else "list"
print(f" {key:<28} {label:<28} <{kind}, {len(value)} entries>")
else:
print(f" {key:<28} {label:<28} {value}")
project = client.projects.create(name="sdk read-steady-state example")
# Resolve by name, or paste explicit IDs: motor_id = "comp_motor_xxx"
motor = client.components.find(name="Spektrum Avian 4260 800Kv")
prop = client.components.find(name="10.5x4.5")
battery = client.components.find(name="Liperior 5000mAh 4S 35C")
# ---- single point ---------------------------------------------------------
sim = client.simulations.create(
project_id=project["id"],
battery_component_id=battery["id"],
airspeed_m_s=0.0,
density_kg_m3=1.225,
battery_charge_pct=100,
rotor_groups=[
{
"label": "main",
"count": 4,
"motor_component_id": motor["id"],
"propeller_component_id": prop["id"],
"throttle_pct": 70,
}
],
)
result = client.simulations.wait(sim["id"], timeout=300)
print(f"single-point status: {result['status']}")
if result["status"] == "completed":
blob = result["result"]
labels = result["display_labels"] # {snake_key: human_string}
# Per-rotor group under its label index ("1", "2", ...).
print_group("rotor 1", blob["1"], labels)
# Aggregate roll-ups across all rotors.
print_group("All", blob["All"], labels)
# The "Battery" entry is passed through as-is: per-cell arrays keyed by human
# strings ("Cell voltages (V)", "Charge levels (%)") that are NOT in
# display_labels — read them directly.
print_group("Battery", blob["Battery"], labels)
# ---- sweep (same schema, one point per grid cell) -------------------------
sweep = client.sweeps.create(
project_id=project["id"],
battery_component_id=battery["id"],
airspeed_m_s=0.0,
density_kg_m3=1.225,
battery_charge_pct=100,
rotor_groups=[
{
"label": "main",
"count": 4,
"motor_component_id": motor["id"],
"propeller_component_id": prop["id"],
"throttle_pct": 70,
}
],
sweep={
"rotor_sweep_mask": [True],
"throttle": {"mode": "range", "start": 10, "stop": 100, "steps": 10},
},
)
sweep_result = client.sweeps.wait(sweep["id"], timeout=600)
print(f"\nsweep status: {sweep_result['status']}")
if sweep_result["status"] == "completed":
# Each point's `rotors` is a single-point result dict; the same snake_case
# keys (and the same display_labels vocabulary read above) apply per point.
print("\npoint | throttle in | per-rotor thrust_n | total_thrust_n")
for pt in client.sweeps.list_points(sweep["id"]):
rotor = pt["rotors"]["1"]
agg = pt["rotors"]["All"]
print(
f" {pt['index']:>2} | {pt['inputs']} | "
f"{rotor['thrust_n']:.2f} | {agg['total_thrust_n']:.2f}"
)
Sweeps use the same schema
Each sweep point exposes a rotors dict with the identical per-rotor / "All" /
"Battery" keys and the same display_labels vocabulary:
for pt in client.sweeps.list_points("sweep_2c5tQ..."):
thrust = pt["rotors"]["1"]["thrust_n"]
print(pt["index"], pt["inputs"], thrust)
See also
- Dynamic outputs — samples and time-series.
client.simulations— running a single point.client.sweeps— parameter studies.