ThrustLab

Creating your own components

A custom component is POST /v1/components with three fields: type (motor | battery | propeller), name, and a type-specific spec_json map. The component is scoped to your account — source="user", visibility="private" — and the response echoes an id (comp_...) you pass to a simulation.

In the Python SDK this is client.components.create:

motor = client.components.create(
    type="motor",
    name="My Custom 4260 800Kv",
    spec_json={ ... },
)

Field names, units, and accepted ranges below match the server-side create validators. The Required column is the set the simulator needs for trustworthy results — the same set the in-app component forms enforce, and the create boundary enforces it too: a create (or full spec_json update) missing a required field is rejected with a 400 naming the field, its meaning, and its units. Unknown keys are stored as-is; the simulator reads only the fields it recognizes.

A successful create or update may additionally carry a non-blocking warnings list (each entry has code, param, message) when a value looks physically implausible — for example a motor R far outside the expected range for its kv, weight, and diameter, which usually means a milliohm/ohm mix-up or a per-phase value where phase-to-phase is expected. The write always succeeds; treat a warning as a prompt to re-check the datasheet.

Motor spec_json

Motor specs are stored as submitted. Supply the full electrical and geometric set below. Inductance L and rotor inertia are auto-estimated from kv, n, diameter, length, and weight when omitted — rotor inertia is required for dynamic (time-domain) simulations, and the estimate is only as good as the dimensions and mass you provide.

FieldMeaningUnitsRequired
kvVelocity constantrev/min per VYes
nMagnet pole countcount (whole number)Yes
RWinding resistance, phase-to-phaseΩYes
LWinding inductanceHEstimated from kv/n/diameter/length if omitted
diameterStator/can diametermmYes — estimation input for L/inertia
lengthMotor body lengthmmYes — estimation input for L/inertia
inertiaRotor inertiakg·m²Estimated from diameter/length/weight if omitted; required for dynamic simulations
weightMotor massgYes — estimation input for inertia
u_nominalNominal voltageVYes
iq_maxMax quadrature currentAYes — or power_max
power_maxMax electrical powerWYes — or iq_max
iq_nominalNo-load / idle currentAYes
topologyoutrunner | inrunnerOptional (default outrunner)
brandBrand labelOptional

R is the phase-to-phase terminal resistance (the value on a motor's datasheet); the solver converts it to per-phase internally.

Battery spec_json

Battery specs are validated at create. A physical pack needs its bounding box, cell layout, capacity, mass, and one internal-resistance figure (c_rating or pack_resistance_mOhm); a source (bench supply) needs only series_cells and source_voltage. form_factor is derived from chemistry when omitted and stored.

FieldMeaningUnitsRequired
chemistrylipo | lihv | nmc | nca | lfp | nimh | sourceYes
length_mmPack lengthmm (1–2000)Yes (physical chemistry)
width_mmPack widthmm (1–2000)Yes (physical chemistry)
height_mmPack heightmm (1–2000)Yes (physical chemistry)
series_cellsCells in series (S)count (≤ 24)Yes
parallel_cellsCells in parallel (P)countYes (physical chemistry)
total_capacity_mAhPack capacitymAhYes (physical chemistry)
c_ratingDischarge C-rating1/hYes (physical chemistry) — or pack_resistance_mOhm
weight_gPack massgYes (physical chemistry)
source_voltageSource output voltageVYes (source chemistry)
v_min_per_cellPer-cell loaded-voltage cutoff overrideVOptional — dynamic (time-domain) runs use this as the depletion cutoff; falls back to the chemistry default when absent
form_factorpouch | cylindricalDerived from chemistry if omitted
cell_modelCylindrical cell (e.g. 21700, AA)Optional (cylindrical only)
pack_styleflat | hump | tOptional (cylindrical only)
internal_resistance_mOhmPer-cell internal resistanceOptional
pack_resistance_mOhmWhole-pack resistanceAlternative to c_rating
brandBrand labelOptional

Validation rules:

  • chemistry must be one of the enum above, else 400.
  • For any non-source chemistry, length_mm / width_mm / height_mm are required, finite, and within 1–2000 mm.
  • form_factor is enum-checked when supplied; derived otherwise (lipo / lihv / lfppouch; nmc / nca / nimhcylindrical).
  • cell_model must belong to the chemistry's catalog when supplied.
  • pack_style requires a cylindrical form_factor.
  • series_cells ≤ 24 and series_cells × parallel_cells ≤ 256.
  • For a physical chemistry, series_cells, parallel_cells, total_capacity_mAh, weight_g, and one of c_rating / pack_resistance_mOhm are required.
  • A source (virtual power source) skips the dimension requirement; it needs only series_cells and source_voltage.

Propeller spec_json

A propeller is defined by its per-station blade geometry — parallel radius / chord / twist arrays, hub → tip — plus its labeled rotation; diameter and pitch name the design. The in-app propeller creator builds these arrays for you. Persisting a propeller requires the hobbyist tier or higher.

FieldMeaningUnitsRequired
diameterPropeller diameterinchYes
pitchPropeller pitchinchYes
num_bladesBlade countcount (1–16)Optional (default 2)
weightPropeller massgYes
maxRPMRated maximum RPMrev/minOptional
inertiaBlade inertia about the spin axiskg·m²Estimated from weight + diameter if omitted; required for dynamic simulations
rotationcw | ccw | both (top view)Yes
radiusPer-station radii, hub → tiplist (≤ 50), mYes
chordPer-station chordslist (≤ 50), mYes
twistPer-station twistlist (≤ 50), degYes
brandBrand labelOptional

Validation rules:

  • num_blades must be a whole number in 1–16, else 422.
  • The per-station arrays (radius / chord / twist / sweep / x_le_over_R) cap at 50 stations; sweep / x_le_over_R must be finite.
  • radius / chord / twist are required as parallel arrays of the same length (≥ 2 stations), with radius positive and strictly increasing hub → tip.
  • rotation must be cw / ccw / both.
  • Persisting a propeller needs creator_edit (hobbyist tier); a free account gets 402.

Worked example

Creates a custom motor, battery, and propeller, then prints each new id:

"""Create your own custom motor, battery, and propeller from spec_json.

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

A custom component is `POST /v1/components` with three fields: `type`
(motor | battery | propeller), `name`, and a type-specific `spec_json` map. The
create is scoped to your account (source="user", visibility="private"); the
returned resource echoes back an `id` (comp_...) you pass to a simulation.

Each create below is `client.components.create(...)`. Field names, units, and
accepted ranges come from the server-side validators — see the reference page
for the full tables.

Notes on validation:
  * Battery: a physical pack needs chemistry, cell layout, capacity, weight_g,
    one resistance figure (c_rating or pack_resistance_mOhm), and bounding-box
    dims; form_factor is derived from chemistry when omitted.
  * Propeller: per-station radius/chord/twist and rotation are required, and
    persisting a propeller needs the hobbyist tier or higher — the create below
    reports the tier gate cleanly on a free account.
"""

from thrustlab import Client
from thrustlab.exceptions import APIError

client = Client()  # reads $THRUSTLAB_API_KEY from the environment

# ---- motor ---------------------------------------------------------------
# Required: kv, n (pole count), R (phase-to-phase winding resistance, ohms),
# weight, diameter, length, u_nominal, iq_nominal, and one of iq_max /
# power_max. Inductance L and rotor inertia are auto-estimated from
# kv/n/diameter/length/weight — inertia is required for dynamic (time-domain)
# runs, so give real dimensions and mass.
motor = client.components.create(
    type="motor",
    name="My Custom 4260 800Kv",
    spec_json={
        "brand": "Custom",
        "kv": 800.0,            # rpm per volt
        "n": 14,                # magnet pole count
        "R": 0.042,             # phase-to-phase winding resistance (ohm)
        "weight": 120.0,        # g
        "diameter": 42.0,       # mm (drives L / inertia estimation)
        "length": 25.0,         # mm
        "u_nominal": 14.8,      # nominal voltage (V)
        "iq_nominal": 1.1,      # no-load / idle current (A)
        "iq_max": 65.0,         # max quadrature current (A)
        "power_max": 960.0,     # max electrical power (W)
        "topology": "outrunner",
    },
)
print(f"created motor {motor['id']}")

# ---- battery -------------------------------------------------------------
# chemistry in {lipo, lihv, nmc, nca, lfp, nimh, source}. A physical pack needs
# its cell layout, capacity, weight_g, one resistance figure (c_rating or
# pack_resistance_mOhm), and length/width/height_mm (1..2000 mm). form_factor
# is derived (lipo -> pouch) when omitted. Pack cap: series <= 24, S*P <= 256.
battery = client.components.create(
    type="battery",
    name="My Custom 4S 5000mAh",
    spec_json={
        "brand": "Custom",
        "chemistry": "lipo",
        "series_cells": 4,
        "parallel_cells": 1,
        "total_capacity_mAh": 5000.0,
        "c_rating": 75.0,
        "weight_g": 480.0,
        "length_mm": 145.0,     # required for a physical chemistry
        "width_mm": 49.0,
        "height_mm": 33.0,
    },
)
print(f"created battery {battery['id']}")

# ---- propeller (hobbyist tier or higher) ---------------------------------
# Required: diameter, pitch, rotation, weight, and the per-station blade
# geometry — parallel radius/chord/twist arrays, hub -> tip (<= 50 stations;
# radius and chord in METERS, twist in degrees). inertia is required for
# dynamic (time-domain) runs and is estimated from weight + diameter when
# omitted.
try:
    prop = client.components.create(
        type="propeller",
        name="My Custom 10.5x4.5",
        spec_json={
            "brand": "Custom",
            "diameter": 10.5,   # inch
            "pitch": 4.5,       # inch
            "num_blades": 2,
            "rotation": "ccw",  # top view; "cw" | "ccw" | "both"
            "weight": 14.0,     # g
            "inertia": 8.3e-5,  # kg*m^2 — required for dynamic runs
            # per-station geometry, hub -> tip (r/R = 0.20 .. 1.00)
            "radius": [0.0267, 0.0400, 0.0600, 0.0800, 0.1000, 0.1200, 0.1334],  # m
            "chord": [0.016, 0.019, 0.022, 0.021, 0.018, 0.014, 0.008],          # m
            "twist": [30.0, 24.0, 17.0, 12.8, 10.3, 8.6, 7.8],                   # deg
        },
    )
    print(f"created propeller {prop['id']}")
except APIError as exc:
    # A free account 402s here (propeller persistence is a creator_edit gate).
    print(f"propeller create skipped: {exc}")

Using a custom component

Pass the returned id to a simulation exactly like a catalog component:

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,
    }],
)

See also

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

Creating your own components · ThrustLab API