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.
| Field | Meaning | Units | Required |
|---|---|---|---|
kv | Velocity constant | rev/min per V | Yes |
n | Magnet pole count | count (whole number) | Yes |
R | Winding resistance, phase-to-phase | Ω | Yes |
L | Winding inductance | H | Estimated from kv/n/diameter/length if omitted |
diameter | Stator/can diameter | mm | Yes — estimation input for L/inertia |
length | Motor body length | mm | Yes — estimation input for L/inertia |
inertia | Rotor inertia | kg·m² | Estimated from diameter/length/weight if omitted; required for dynamic simulations |
weight | Motor mass | g | Yes — estimation input for inertia |
u_nominal | Nominal voltage | V | Yes |
iq_max | Max quadrature current | A | Yes — or power_max |
power_max | Max electrical power | W | Yes — or iq_max |
iq_nominal | No-load / idle current | A | Yes |
topology | outrunner | inrunner | — | Optional (default outrunner) |
brand | Brand label | — | Optional |
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.
| Field | Meaning | Units | Required |
|---|---|---|---|
chemistry | lipo | lihv | nmc | nca | lfp | nimh | source | — | Yes |
length_mm | Pack length | mm (1–2000) | Yes (physical chemistry) |
width_mm | Pack width | mm (1–2000) | Yes (physical chemistry) |
height_mm | Pack height | mm (1–2000) | Yes (physical chemistry) |
series_cells | Cells in series (S) | count (≤ 24) | Yes |
parallel_cells | Cells in parallel (P) | count | Yes (physical chemistry) |
total_capacity_mAh | Pack capacity | mAh | Yes (physical chemistry) |
c_rating | Discharge C-rating | 1/h | Yes (physical chemistry) — or pack_resistance_mOhm |
weight_g | Pack mass | g | Yes (physical chemistry) |
source_voltage | Source output voltage | V | Yes (source chemistry) |
v_min_per_cell | Per-cell loaded-voltage cutoff override | V | Optional — dynamic (time-domain) runs use this as the depletion cutoff; falls back to the chemistry default when absent |
form_factor | pouch | cylindrical | — | Derived from chemistry if omitted |
cell_model | Cylindrical cell (e.g. 21700, AA) | — | Optional (cylindrical only) |
pack_style | flat | hump | t | — | Optional (cylindrical only) |
internal_resistance_mOhm | Per-cell internal resistance | mΩ | Optional |
pack_resistance_mOhm | Whole-pack resistance | mΩ | Alternative to c_rating |
brand | Brand label | — | Optional |
Validation rules:
chemistrymust be one of the enum above, else400.- For any non-
sourcechemistry,length_mm/width_mm/height_mmare required, finite, and within 1–2000 mm. form_factoris enum-checked when supplied; derived otherwise (lipo/lihv/lfp→pouch;nmc/nca/nimh→cylindrical).cell_modelmust belong to the chemistry's catalog when supplied.pack_stylerequires a cylindricalform_factor.series_cells≤ 24 andseries_cells × parallel_cells≤ 256.- For a physical chemistry,
series_cells,parallel_cells,total_capacity_mAh,weight_g, and one ofc_rating/pack_resistance_mOhmare required. - A
source(virtual power source) skips the dimension requirement; it needs onlyseries_cellsandsource_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.
| Field | Meaning | Units | Required |
|---|---|---|---|
diameter | Propeller diameter | inch | Yes |
pitch | Propeller pitch | inch | Yes |
num_blades | Blade count | count (1–16) | Optional (default 2) |
weight | Propeller mass | g | Yes |
maxRPM | Rated maximum RPM | rev/min | Optional |
inertia | Blade inertia about the spin axis | kg·m² | Estimated from weight + diameter if omitted; required for dynamic simulations |
rotation | cw | ccw | both (top view) | — | Yes |
radius | Per-station radii, hub → tip | list (≤ 50), m | Yes |
chord | Per-station chords | list (≤ 50), m | Yes |
twist | Per-station twist | list (≤ 50), deg | Yes |
brand | Brand label | — | Optional |
Validation rules:
num_bladesmust be a whole number in 1–16, else422.- The per-station arrays (
radius/chord/twist/sweep/x_le_over_R) cap at 50 stations;sweep/x_le_over_Rmust be finite. radius/chord/twistare required as parallel arrays of the same length (≥ 2 stations), withradiuspositive and strictly increasing hub → tip.rotationmust becw/ccw/both.- Persisting a propeller needs
creator_edit(hobbyist tier); a free account gets402.
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
client.components— browsing the catalog.- Steady-state outputs — reading a result.