client.simulations
Run a single-point steady-state simulation: one airspeed, one density, one
battery state-of-charge, one or more rotor groups. Simulations are
asynchronous — create returns immediately with status: "queued".
Methods
| Method | Endpoint | Returns |
|---|---|---|
client.simulations.create(project_id=..., **body) | POST /v1/simulations | New simulation in queued state |
client.simulations.list(project_id=..., status=...) | GET /v1/simulations | CursorPager of simulations |
client.simulations.retrieve(simulation_id) | GET /v1/simulations/{id} | Single simulation (current status) |
client.simulations.cancel(simulation_id) | POST /v1/simulations/{id}/cancel | Simulation in canceled state |
client.simulations.wait(simulation_id, timeout=600) | (polls retrieve) | Final simulation when terminal |
list's status= filters to one wire status (draft, queued, running,
completed, failed, or canceled); an unrecognized value 422s. project_id
maps to the server's project query parameter.
A few endpoints have no SDK method yet — call them with your own HTTP client (see Authentication for the API key header):
| Endpoint | Purpose |
|---|---|
PATCH /v1/simulations/{id} | Rename, star/unstar, or replace plot_config_json |
DELETE /v1/simulations/{id} | Delete a non-active (not queued/running) simulation |
POST /v1/simulations/{id}/promote | Promote a draft to queued/running with the (possibly edited) body |
POST /v1/simulations/run-queued | Dispatch every queued simulation as warm batches grouped by rotor count |
POST /v1/simulations/run-selected | Dispatch a caller-selected subset of queued simulations |
Request body
simulations.create forwards every keyword argument as the JSON body. The
required shape:
sim = client.simulations.create(
project_id="proj_2c5tQ...",
battery_component_id="comp_batt_xxx",
airspeed_m_s=0.0, # forward velocity (0 = hover)
density_kg_m3=1.225, # ISA sea level
battery_charge_pct=100, # 0..100
rotor_groups=[{
"label": "main",
"count": 4,
"motor_component_id": "comp_motor_xxx",
"propeller_component_id": "comp_prop_xxx",
"throttle_pct": 70, # 0..100
"esc_resistance_mohm": 0,
"esc_motor_wire_resistance_mohm": 0,
}],
)
A rotor_groups array supports mixed-rotor designs: a coaxial-twin with two
different propellers is two groups with count: 1 and different
propeller_component_ids. A quad-X with four identical rotors is one group
with count: 4. See the API reference
for every field, including optional ESC presets and custom resistance modes.
Caps
rotor_groups holds at most 10 entries. Each group's count is 1–16, and the
sum of every group's count in the simulation cannot exceed 16.
Flight condition
inflow_mode picks how the flight condition enters the solve — the two modes
are mutually exclusive and mixing them 422s:
inflow_mode | Sim-level field | Per-group field |
|---|---|---|
"ground" (default) | vertical_speed_m_s (±30 m/s, default 0), alongside airspeed_m_s | tilt_deg (0–90°, default 0) — rotor-axis tilt from vertical |
"components" | — | v_axial_m_s (−30..100 m/s), v_edge_m_s (0..100 m/s, required together) |
"ground" rejects any group's v_axial_m_s/v_edge_m_s; "components"
rejects vertical_speed_m_s and any group's tilt_deg, and requires both
v_axial_m_s and v_edge_m_s on every group.
Custom component overrides
Swap in an ad-hoc motor, propeller, or battery without a saved component —
custom_motor / custom_propeller per rotor group, or custom_battery
sim-level:
"custom_propeller": {
"base_component_id": "comp_prop_xxx", # optional starting point; omit to build from scratch
"name": "Modified 10x5E",
"spec_json": {...}, # full component spec
}
Per-group fields
Beyond motor_component_id / propeller_component_id / throttle_pct, each
rotor group accepts:
| Field | Type | Default | Notes |
|---|---|---|---|
motor_cooling_source | "cowling" | "prop_exit_velocity" | "custom" | prop_exit_velocity | Still air / prop-slipstream forced convection / custom |
motor_cooling_velocity_m_s | float, 0–200 | None | Only with motor_cooling_source="custom"; 422 otherwise |
motor_r_th | float, 0–100 (K/W) | None | Direct thermal-resistance override; only with motor_cooling_source="custom" |
motor_t_w / motor_t_mag | float | None | Pin winding/magnet temperature (°C); omit to let the model solve it |
esc_type | "foc" | "six_step" | foc | ESC commutation type |
esc_timing | "low" | "medium" | "high" | "auto" | medium | Six-step commutation advance; ignored under FOC |
esc_pwm_frequency_khz | float, 8–48 | 24.0 | ESC switching frequency |
esc_sync_rectification | bool | True | Synchronous rectification |
The sim-level battery_esc_wire_resistance_mohm (default 0) and name
(optional display name) round out the body.
Thermal & environment inputs
| Field | Default | Bounds | Notes |
|---|---|---|---|
ambient_temp_c | 25.0 | −60..85 | °C |
flight_regime | "static_bench" | static_bench | prop_wash_mild | prop_wash_strong | forced_air | Convective cooling regime; superseded by cooling_source when set |
cooling_source | None | "static" | "airspeed" | "prop_slipstream" | "forced_air" | Battery convection source; None falls back to flight_regime |
forced_air_velocity_m_s | None | 0–200 | Only meaningful with cooling_source="forced_air" |
flight_duration | 60.0 | 0..86400, or null | Seconds for the time-bounded thermal solve; null disables time-bounding |
Launch intent
launch_intent controls what create actually does:
| Value | Behavior |
|---|---|
"run" (default) | Reserves compute units and dispatches immediately |
"queue" | Reserves compute units but defers dispatch (run later via the batch endpoints) |
"draft" | Persists with no compute-unit reservation, no dispatch, and no rotor-group requirement |
Run synchronously (wait)
The most common pattern: submit, then block until the simulation is in a
terminal state. wait polls retrieve at poll_interval= seconds (default
2.0) until status leaves {"queued", "running"}, or until timeout=
seconds elapse.
"""Single-point simulation: auth -> submit -> wait() -> read the canonical result.
Run it:
export THRUSTLAB_API_KEY=key_... # never hard-code the key
python examples/simulations/run_sync.py
The verification combo below (Spektrum Avian 4260 800 kV + 10.5x4.5 + Liperior
4S 5000 mAh @ 70% throttle, static) converges to ~9.78 N / ~8645 rpm. 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
project = client.projects.create(name="sdk single-point example")
# Resolve components by name (find() returns the single match or raises
# AmbiguousComponentError / NotFoundError). Or paste explicit IDs instead:
# 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")
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,
}
],
)
print(f"created {sim['id']}, status={sim['status']}")
# wait() polls until completed / failed / canceled (or a timeout sentinel).
result = client.simulations.wait(sim["id"], timeout=300)
print(f"final status: {result['status']}")
if result["status"] == "completed":
# Canonical snake_case result (post-D-01): per-rotor group under its label
# index ("1", "2", ...); aggregate roll-ups under "All".
per_rotor = result["result"]["1"]
aggregate = result["result"]["All"]
print(f"per-rotor thrust: {per_rotor['thrust_n']:.2f} N")
print(f"per-rotor rpm: {per_rotor['rpm']:.0f}")
print(f"total thrust: {aggregate['total_thrust_n']:.2f} N")
If wait times out before the server finishes, it returns the resource
with status: "timed_out" — an SDK-side sentinel, never a server
state. The server-side run keeps going; call wait() or retrieve()
again to resume polling.
result = client.simulations.wait(sim["id"], timeout=60)
if result["status"] == "timed_out":
# Resume later — the server is still computing.
final = client.simulations.retrieve(sim["id"])
Terminal status values
| Status | Meaning |
|---|---|
completed | Result fields populated under result; ready to read. |
failed | Inspect error.code and error.message. |
canceled | User-canceled before completion (one 'l' on the wire). |
timed_out | SDK-side wait() sentinel — the server run is still going. |
Cancel an in-flight simulation
client.simulations.cancel("sim_2c5tQ...")
Cancellation is best-effort: a simulation already in its final ms of compute
may still finish as completed. The cancel call is idempotent — calling it
on an already-terminal simulation is a no-op that returns the current state.
Read the result
A completed simulation has its computed outputs under result, in
canonical snake_case: per-rotor-group blocks under "1"/"2"/… and
whole-vehicle aggregates under "All". A sibling display_labels map
translates every key to its human label (full field list in the
API reference):
sim = client.simulations.retrieve("sim_2c5tQ...")
if sim["status"] == "completed":
r = sim["result"]["1"] # per-rotor values for the first group
agg = sim["result"]["All"] # whole-vehicle aggregates
print(f"thrust: {r['thrust_n']:.2f} N")
print(f"current: {r['current_a']:.2f} A")
print(f"rpm: {r['rpm']:.0f}")
print(f"total thrust: {agg['total_thrust_n']:.2f} N")
List simulations for a project
for sim in client.simulations.list(project_id="proj_2c5tQ...", status="failed"):
print(sim["id"], sim["status"], sim["created_at"])
Without project_id you get every simulation owned by the calling user
across all projects.
Recipe: fire-and-forget via webhooks
For long jobs or batch pipelines, skip polling — register a webhook endpoint
and let the server push a simulation.completed event when it's done.
endpoint = client.webhook_endpoints.create(
url="https://your-app.example.com/webhooks/thrustlab",
events=["simulation.completed", "simulation.failed"],
)
sim = client.simulations.create(project_id="proj_xxx", ...)
# Your webhook handler will receive the event when terminal.
See Webhooks for endpoint management and signature verification for the receiving handler.
See also
- Async resources — polling vs webhooks
- Sweeps — multi-point parameter studies
- Components — finding
comp_motor_*/comp_prop_*/comp_batt_*ids