ThrustLab

Dynamic outputs

A dynamic simulation integrates the powertrain through a throttle/airspeed schedule until a termination condition. A completed run carries a result blob plus a sibling display_labels map (the same {snake_key: human_string} map as a steady-state result — it applies to each per-step sample).

Result shape

result is a dict with these top-level keys:

KeyContents
scorecardWhole-run headline metrics.
samplesOne full steady-shaped snapshot per saved step.
series{channel: list[float]} aligned to series["time_s"] — the downsampled display copy (the plotting source).
series_rawSame shape as series, at full solver resolution (not downsampled) — this is what GET .../export.csv reads.
eventsTimestamped run events.
run_metaStep counts and depletion bookkeeping.
derived{ "n_rotors": <int> }.
timeThe saved time grid, seconds (mirrors series["time_s"]).
dyn = client.dynamic_simulations.retrieve("dyn_2c5tQ...")
blob = dyn["result"]
labels = dyn["display_labels"]

blob["scorecard"]["flight_time_s"]        # run duration, seconds
blob["samples"][-1]["All"]["total_thrust_n"]  # final total thrust, N
blob["series"]["total_current_a"]         # per-step current channel, A

scorecard

Whole-run headline metrics (canonical snake_case).

KeyUnitsMeaning
flight_time_ssRun duration (to depletion or schedule end).
energy_whWhEnergy delivered over the run.
range_mmDistance covered (airspeed integral).
peak_current_aAMaximum total pack current.
min_cell_voltage_vVLowest cell voltage reached.
peak_winding_temp_c°CHottest winding temperature reached.
avg_efficiencyFractionMean powertrain efficiency.
depletion_criterionWhich cutoff ended the run (e.g. soc), or null.
terminated_earlyBooleanWhether a cutoff stopped the run before the schedule ended.

Temperature and cell-voltage entries are null when the corresponding solve did not run (e.g. a thermal-off run has no peak_winding_temp_c).

samples

Each entry is a full steady-shaped snapshot at one saved step: the same per-rotor ("1", "2", …), "All", and "Battery" keys as a single-point result, and the same display_labels vocabulary. See Steady-state outputs for the per-key tables.

first, last = blob["samples"][0], blob["samples"][-1]
print(first["All"]["total_thrust_n"], "→", last["All"]["total_thrust_n"])
print(labels["total_thrust_n"])   # "Total Thrust (N)"

series

A flat {channel: list[float]} map. Every channel is aligned index-for-index to series["time_s"], so channel i is the value at time_s[i].

ChannelUnits
time_ss (the shared time axis)
total_thrust_nN
total_current_aA
total_voltage_vV
min_cell_soc_pct%
max_winding_temp_c°C
max_core_temp_c°C
airspeed_msm/s (scheduled airspeed at the saved instant)
vertical_speed_msm/s, signed (scheduled vertical speed; 0 for a schedule with no vertical_speed_target)

Per-rotor channels are named rotor<N>_<field> for each rotor group, when the engine supplied them:

ChannelUnits
rotor1_rpmrev/min
rotor1_current_aA
rotor1_thrust_nN
rotor1_motor_vV
rotor1_torque_nmN·m
rotor1_shaft_wW
rotor1_aero_wW (propeller aero power)
rotor1_elec_wW
rotor1_t_w_c°C (winding)
rotor1_t_mag_c°C (magnet)
rotor1_r_th_k_per_wK/W (effective cooled thermal resistance at that instant)
rotor1_cooling_v_msm/s (motor cooling-air velocity at that instant)
rotor1_throttle_pct%
rotor1_tilt_degdeg (rotor-axis tilt from vertical at that instant)
rotor1_v_axial_msm/s (decomposed axial inflow at that instant)
rotor1_v_edge_msm/s (decomposed edgewise inflow at that instant, ≥ 0)

Read the available channels off the keys rather than hard-coding them — a thermal-off or single-rotor run omits the channels it did not compute:

series = blob["series"]
print(sorted(series.keys()))
t, thrust = series["time_s"], series["total_thrust_n"]

events

A list of timestamped events. Each entry is { "t": <seconds>, "type": <str>, "severity": <str>, "detail": <dict> }.

typeseverityDetail
depletioninfo{ "criterion": ... } — the run hit a cutoff.
segment_boundaryinfoA schedule segment transition.
in_rush_peakinfo{ "current_a": ... } — timestamped max current.
thermal_thresholdwarning{ "node": winding|magnet|core, "limit_c": ... }.
step_cap_hitinfoThe step ceiling was reached without a cutoff.
non_convergencewarningA non-finite sample was detected.

run_meta

KeyMeaningUnits
stepsNumber of integrated steps (full resolution).count
save_dtSave interval.s
depletion_tTime of depletion (or null).s
depletion_criterionWhich cutoff tripped (or null).
terminated_earlyWhether a cutoff stopped the run early.
step_cap_hitWhether the step ceiling was reached.
n_chunksNumber of continuation windows the run was integrated in (an until-depleted run that outlives one window continues in more; a fixed-duration run is always 1).count
max_sim_time_cappedWhether the run hit the server's hard wall-time cap and stopped before true depletion.

Worked example

Runs a dynamic simulation, then reads the scorecard, a couple of series channels, and the events:

"""Read a dynamic (time-domain) result: samples, time-series, and the scorecard.

Run it:
    export THRUSTLAB_API_KEY=key_...        # never hard-code the key
    python examples/outputs/read_dynamic.py

A completed dynamic run integrates the powertrain through a throttle/airspeed
schedule until a termination condition. Its `result` blob carries:
  * scorecard   — whole-run headline metrics (flight_time_s, peak_current_a,
                  min_cell_voltage_v, peak_winding_temp_c, avg_efficiency, ...).
  * samples[]    — one full steady-shaped snapshot per saved step; each entry has
                  the SAME canonical per-rotor / "All" / "Battery" keys as a
                  single-point result, so display_labels applies to it too.
  * series       — {channel_name: list[float]} aligned to series["time_s"], the
                  down-sampled plotting source (total_thrust_n, total_current_a,
                  rotor1_rpm, ...).
  * events / run_meta — timestamped run events + step/depletion bookkeeping.

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 read-dynamic 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")

dyn = client.dynamic_simulations.create(
    project_id=project["id"],
    battery_component_id=battery["id"],
    density_kg_m3=1.225,
    battery_charge_pct=100,
    ambient_temp_c=25,
    rotor_groups=[
        {
            "label": "main",
            "count": 4,
            "motor_component_id": motor["id"],
            "propeller_component_id": prop["id"],
        }
    ],
    # Ramp to 70% over 2 s (soft-start — a throttle step onto a stationary
    # rotor sags the pack below the low-voltage cutoff), then hold for 20 s.
    schedule={
        "mode": "segments",
        "segments": [
            {
                "duration_s": 2.0,
                "airspeed_target": 0.0,
                "per_group": {"main": {"throttle_target": 70, "throttle_ramp": "linear"}},
            },
            {
                "duration_s": 20.0,
                "airspeed_target": 0.0,
                "per_group": {"main": {"throttle_target": 70}},
            },
        ],
    },
    termination={"mode": "fixed"},
)
result = client.dynamic_simulations.wait(dyn["id"], timeout=600)
print(f"dynamic status: {result['status']}")

if result["status"] == "completed":
    blob = result["result"]
    labels = result["display_labels"]  # applies to each samples[] entry

    # scorecard: whole-run headline metrics (canonical snake_case).
    print("\n[scorecard]")
    for key, value in blob["scorecard"].items():
        print(f"  {key:<24} {value}")

    # samples[]: each entry is a full steady-shaped snapshot — read the same
    # per-rotor / "All" keys (and display_labels) as a single-point result.
    samples = blob["samples"]
    first, last = samples[0], samples[-1]
    print(f"\nsteps: {len(samples)}")
    print(f"  t0 total_thrust_n: {first['All']['total_thrust_n']:.2f} "
          f"({labels['total_thrust_n']})")
    print(f"  tN total_thrust_n: {last['All']['total_thrust_n']:.2f}")

    # series: down-sampled channels aligned to series["time_s"]. Print a couple.
    series = blob["series"]
    print("\n[series channels]")
    print(f"  available: {sorted(series.keys())}")
    time_s = series["time_s"]
    thrust = series.get("total_thrust_n", [])
    current = series.get("total_current_a", [])
    print("\n  t (s) | total_thrust_n | total_current_a")
    for i in range(0, len(time_s), max(1, len(time_s) // 5)):
        t = time_s[i]
        th = thrust[i] if i < len(thrust) else float("nan")
        cu = current[i] if i < len(current) else float("nan")
        print(f"  {t:6.1f} | {th:>14.2f} | {cu:>15.2f}")

    # events: timestamped run events (depletion, in_rush_peak, thermal_threshold).
    print("\n[events]")
    for ev in blob["events"]:
        print(f"  t={ev['t']:.1f}s  {ev['type']:<18} {ev['severity']:<8} {ev['detail']}")

See also

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

Dynamic outputs · ThrustLab API