Sweeps resource
Accessor: client.sweeps
A sweep is a parameter study: pick a base configuration plus one or more parameter ranges, and the server computes a simulation at every combination. Use sweeps to plot thrust vs throttle curves, compare propellers across a fixed motor, or build hover-time vs payload tables.
Like simulations, sweeps are asynchronous — create returns
status: "queued" immediately.
Methods
| Method | Endpoint | Returns |
|---|---|---|
client.sweeps.create(project_id=..., **body) | POST /v1/sweeps | New sweep in queued state |
client.sweeps.list(project_id=...) | GET /v1/sweeps | CursorPager of sweeps |
client.sweeps.retrieve(sweep_id) | GET /v1/sweeps/{id} | Single sweep (current status) |
client.sweeps.cancel(sweep_id) | POST /v1/sweeps/{id}/cancel | Sweep in canceled state |
client.sweeps.list_points(sweep_id) | GET /v1/sweeps/{id}/points | CursorPager of computed points |
client.sweeps.wait(sweep_id, timeout=600) | (polls retrieve) | Final sweep when terminal |
Request body
sweeps.create takes the same base configuration as a simulation, plus a
sweep object naming the axes to vary:
sweep = client.sweeps.create(
project_id="proj_2c5tQ...",
battery_component_id="comp_batt_xxx",
airspeed_m_s=0.0,
density_kg_m3=1.225,
battery_charge_pct=100,
rotor_groups=[{
"label": "main", "count": 4,
"motor_component_id": "comp_motor_xxx",
"propeller_component_id": "comp_prop_xxx",
"throttle_pct": 70, # baseline; the swept axis overrides it
}],
sweep={
"rotor_sweep_mask": [True], # which rotor groups sweep the throttle axis
"throttle": {"mode": "range", "start": 10, "stop": 100, "steps": 10},
},
)
Numeric axes (throttle, airspeed, density, battery_charge,
esc_pwm_frequency_khz, vertical_speed, tilt) each take
{"mode": "range", "start": ..., "stop": ..., "steps": ...} or
{"mode": "custom", "values": [...]}.
steps is the NUMBER OF POINTS, not a step size — the name reads the other
way. {"start": 10, "stop": 100, "steps": 10} gives 10, 20, 30 … 100: an
inclusive linspace from start to stop, not "step by 10". steps accepts 2
to 1000. {"mode": "custom", "values": [...]} takes an explicit list instead.
esc_timing_values is an explicit list of timing presets, and component_axes
sweeps categorical motor/propeller/battery choices. Multiple axes form a
Cartesian grid: 2 axes of 5 values each = 25 simulation points.
sweep also requires rotor_sweep_mask — a bool per rotor group, same
length as rotor_groups — even when no axis needs it. At least one axis (or
a non-empty component_axes) must actually be armed, or the sweep 422s with
"At least one parameter must be swept".
The tilt axis needs an extra arming step: a tilt range alone does
nothing — sweep.tilt_sweep_mask (bool per rotor group, same length as
rotor_groups) selects which groups sweep it. Each masked group is an
independent grid dimension. vertical_speed and tilt are ground-mode
only (see Flight condition below) — under
inflow_mode="components" either one 422s.
See API reference for the full body schema.
Component axes
A component axis sweeps a categorical choice: which motor, which propeller, which battery.
The shape is {"axis": "motor"|"propeller"|"battery", "slots": [<rotor group index>, ...], "component_ids": [...]}.
It needs at least two ids; a one-value axis is not a sweep and is rejected with
invalid_component_axis.
slots indexes rotor_groups positionally. More than one slot makes it ONE
shared grid dimension applied to each of those slots, not a cross-product. A
battery axis must target exactly one slot. slot (singular, default 0) is the
older single-slot spelling; prefer slots.
A component axis crosses the numeric axes like any other dimension, so four propellers times four throttles is one 16-point grid. The alternative, a loop of single-point submissions, spends the per-account submission limit and recompiles the solver for every point. See Rate limits.
Each computed point names the component it was solved with at
point["inputs"]["component"], a list of {axis, slot, id, name}. On the sweep
resource, sweep_config.component_axes[] reads back as
{axis, slot, slots, components: [{id, name}]}. Names and ids only — a sweep
never returns a catalog component's spec values.
"""Component axis x throttle: auth -> submit -> wait() -> inspect points.
Run it:
export THRUSTLAB_API_KEY=key_... # never hard-code the key
python examples/sweeps/component_axis_grid.py
Crosses a propeller component axis with a throttle range, so one request solves
every propeller at every throttle. It takes the first four catalog propellers
from client.components.list(type="propeller") and crosses them with four
throttle settings: 16 points in one submission.
Each returned point names the propeller that produced it at
point["inputs"]["component"], a list of {axis, slot, id, name}.
Sixteen single-point submissions would be sixteen requests against the
per-minute submission limit and sixteen cold solves; this is one request and
one warm solver.
`sweep.throttle.steps` is the NUMBER OF POINTS, not a step size.
"""
from thrustlab import Client
client = Client() # reads $THRUSTLAB_API_KEY from the environment
project = client.projects.create(name="sdk component-axis example")
# The fixed half of the configuration — same verification combo as
# examples/sweeps/run_and_poll.py. Resolve by name, or paste explicit IDs.
motor = client.components.find(name="BadAss 2826-820Kv")
battery = client.components.find(
name="Liperior 5000mAh 4S 35C 14.8V Lipo Battery With XT90 Plug"
)
# The swept half: the first four catalog propellers. `.data` is the first page
# of the cursor pager, so this is one request. A component axis needs at least
# two IDs — a one-value axis is not a sweep, and the server rejects it with
# `invalid_component_axis`.
props = client.components.list(type="propeller", limit=4).data
prop_ids = [p["id"] for p in props]
print("propeller axis:", ", ".join(p["name"] for p in props))
sweep = client.sweeps.create(
project_id=project["id"],
battery_component_id=battery["id"],
airspeed_m_s=0.0,
density_kg_m3=1.225,
battery_charge_pct=100,
# `propeller_component_id` here is the BASE value for slot 0. The component
# axis below overrides it at every point, so it only has to be valid.
rotor_groups=[
{
"label": "main",
"count": 4,
"motor_component_id": motor["id"],
"propeller_component_id": prop_ids[0],
"throttle_pct": 70,
}
],
sweep={
# One bool per rotor group: this group follows the swept throttle axis.
"rotor_sweep_mask": [True],
# `steps` is the NUMBER OF POINTS, not a step size: 40, 60, 80, 100.
"throttle": {"mode": "range", "start": 40, "stop": 100, "steps": 4},
# `slots` indexes rotor_groups. Two or more slots would make this ONE
# shared grid dimension applied to each of them, not a cross-product.
"component_axes": [
{"axis": "propeller", "slots": [0], "component_ids": prop_ids},
],
},
)
print(f"created {sweep['id']}: {sweep['total_points']} points in one request")
result = client.sweeps.wait(sweep["id"], timeout=900)
print(f"final status: {result['status']}")
if result["status"] == "completed":
print("\npropeller | throttle | thrust/rotor (N)")
for pt in client.sweeps.list_points(sweep["id"]):
# `inputs["component"]` names the component(s) this point was solved
# with: one {axis, slot, id, name} entry per active component axis.
# It carries names and IDs only — never the component's spec values.
chosen = ", ".join(c["name"] for c in pt["inputs"]["component"])
thrust = pt["rotors"]["1"]["thrust_n"]
print(f" {chosen:<18} | {pt['inputs']['throttle']:>7}% | {thrust:.2f}")
Launch intent and parked sweeps
| Value | Behavior |
|---|---|
"run" (default) | Reserves compute units and dispatches immediately. |
"queue" | Reserves compute units and parks the sweep. dispatched_at stays null. |
"draft" | Persists with no compute-unit reservation and no dispatch. |
A parked sweep is dispatched by client.simulations.run_queued() (every parked
run you own, across all projects) or client.simulations.run_selected([...]) (a
named subset). Both accept sweep_ ids even though they live on the
simulations resource.
The per-minute submission limit counts POST /v1/sweeps calls, including
parked sweeps. Parking controls when the work runs. It does not avoid the
submission limit. Pace the create calls, or put more points into fewer sweeps
by adding axes to one grid instead of submitting many.
Dynamic runs have no parked state.
What the sweep resource tells you
input_snapshot is the request body as submitted. When the caller omits it,
the server stores the body it received, so a stored sweep can be read back
exactly as it was asked for. It used to be null for sweeps, which meant a
field like cooling_source or flight_duration could not be recovered from
the resource at all.
credits_breakdown is {points, rotors, cu_per_point_per_rotor, total}. The
cost of a grid is points x rotors x cu_per_point_per_rotor, readable rather
than inferred from a balance change.
total_points and completed_points give progress; the fraction is
completed_points / total_points.
Timestamps are created_at, dispatched_at, started_at and completed_at.
Queue wait is started_at - dispatched_at. See
Async resources.
cancel_requested_at is set when a cancel has been accepted for a run that is
still going.
Flight condition
sweeps.create accepts the same inflow_mode as a single-point simulation
("ground" default, or "components") — see
Simulations: Flight condition
for the field-level rules. vertical_speed and tilt sweep axes only make
sense under "ground" mode; a sweep with inflow_mode="components" that sets
either one 422s.
Run synchronously (wait + list_points)
The standard pattern: submit, wait for terminal, then iterate the points.
list_points returns a cursor pager — let it
auto-iterate so you don't have to manage the cursor by hand.
sweep = client.sweeps.create(project_id="proj_xxx", ...)
result = client.sweeps.wait(sweep["id"], timeout=600, poll_interval=5.0)
if result["status"] == "completed":
for pt in client.sweeps.list_points(sweep["id"]):
print(pt["inputs"]["throttle"], "→", pt["rotors"]["1"]["thrust_n"], "N")
poll_interval=5.0 is a sane setting for sweeps — points compute in batches
and the resource updates progressively.
Read points + plot
import matplotlib.pyplot as plt
xs, ys = [], []
for pt in client.sweeps.list_points("sweep_2c5tQ..."):
xs.append(pt["inputs"]["throttle"])
ys.append(pt["rotors"]["1"]["thrust_n"])
plt.plot(xs, ys, marker="o")
plt.xlabel("throttle (%)")
plt.ylabel("per-rotor thrust (N)")
plt.title("Quad-X hover thrust")
plt.show()
Each point carries index, inputs (the axis values for that point, e.g.
{"throttle": 40.0, "airspeed": 0.0, ...}), and rotors — the same
canonical snake_case shape as a single-point result: per-group blocks
under "1"/"2"/… plus "All"/"Battery" aggregates.
The human-label map lives on the points page envelope (GET /v1/sweeps/{id}/points), not on the sweep resource itself — and
list_points's CursorPager only exposes .data, not the raw envelope, so
display_labels isn't reachable through it. To read it, call the points
endpoint directly (or see
Steady-state outputs — a sweep point
uses the identical key vocabulary as a single-point result's display_labels).
Cancel an in-flight sweep
client.sweeps.cancel("sweep_2c5tQ...")
A queued sweep cancels immediately: status comes back canceled and the
compute units are refunded.
A running sweep gets 202 Accepted with status still "running" and
cancel_requested_at set. The request is recorded durably; the worker stops
after the point it is computing and the row turns canceled then. Poll
retrieve or wait. cancel_requested_at is what makes a pending cancel
readable after a reload — before it existed a client had to remember locally
that it had asked.
A draft sweep cannot be canceled (409 — cancel applies to a dispatched run);
delete it instead with DELETE /v1/sweeps/{id} (no SDK wrapper yet).
To cancel many sweeps in one call — a whole grid you queued, say — use
client.simulations.cancel_selected([...]) or
client.simulations.cancel_queued(project_id=...); both take sweep ids. See
Cancel many runs at once.
List sweeps for a project
for sweep in client.sweeps.list(project_id="proj_2c5tQ..."):
print(sweep["id"], sweep["status"])
See also
- Async resources — sweeps poll the same way as simulations
- Simulations — the request body shape matches one-to-one
- Pagination —
list_pointscursor contract