ThrustLab

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 asynchronouscreate returns immediately with status: "queued".

Methods

MethodEndpointReturns
client.simulations.create(project_id=..., **body)POST /v1/simulationsNew simulation in queued state
client.simulations.list(project_id=..., status=...)GET /v1/simulationsCursorPager of simulations
client.simulations.retrieve(simulation_id)GET /v1/simulations/{id}Single simulation (current status)
client.simulations.cancel(simulation_id)POST /v1/simulations/{id}/cancelSimulation 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):

EndpointPurpose
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}/promotePromote a draft to queued/running with the (possibly edited) body
POST /v1/simulations/run-queuedDispatch every queued simulation as warm batches grouped by rotor count
POST /v1/simulations/run-selectedDispatch 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_modeSim-level fieldPer-group field
"ground" (default)vertical_speed_m_s (±30 m/s, default 0), alongside airspeed_m_stilt_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:

FieldTypeDefaultNotes
motor_cooling_source"cowling" | "prop_exit_velocity" | "custom"prop_exit_velocityStill air / prop-slipstream forced convection / custom
motor_cooling_velocity_m_sfloat, 0–200NoneOnly with motor_cooling_source="custom"; 422 otherwise
motor_r_thfloat, 0–100 (K/W)NoneDirect thermal-resistance override; only with motor_cooling_source="custom"
motor_t_w / motor_t_magfloatNonePin winding/magnet temperature (°C); omit to let the model solve it
esc_type"foc" | "six_step"focESC commutation type
esc_timing"low" | "medium" | "high" | "auto"mediumSix-step commutation advance; ignored under FOC
esc_pwm_frequency_khzfloat, 8–4824.0ESC switching frequency
esc_sync_rectificationboolTrueSynchronous rectification

The sim-level battery_esc_wire_resistance_mohm (default 0) and name (optional display name) round out the body.

Thermal & environment inputs

FieldDefaultBoundsNotes
ambient_temp_c25.0−60..85°C
flight_regime"static_bench"static_bench | prop_wash_mild | prop_wash_strong | forced_airConvective cooling regime; superseded by cooling_source when set
cooling_sourceNone"static" | "airspeed" | "prop_slipstream" | "forced_air"Battery convection source; None falls back to flight_regime
forced_air_velocity_m_sNone0–200Only meaningful with cooling_source="forced_air"
flight_duration60.00..86400, or nullSeconds for the time-bounded thermal solve; null disables time-bounding

Launch intent

launch_intent controls what create actually does:

ValueBehavior
"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

StatusMeaning
completedResult fields populated under result; ready to read.
failedInspect error.code and error.message.
canceledUser-canceled before completion (one 'l' on the wire).
timed_outSDK-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

We use cookies and analytics tools to understand how you use ThrustLab and improve the experience. Privacy Policy

Python SDK — Simulations · ThrustLab API