# Autopilot
Fly a Crazyflie across a procedurally generated world, find the landing pad inside a noisy search area, and touch down clean: `cf_autopilot`, the navigation family of Swarm Subnet 124.
---
Table of Contents
- Mission
- Interface
- State vector layout
- Maps
- Episode rules
- Scoring
- Runtime limits
- Local testing
---
## Mission
One drone, one goal pad, 60 seconds. Your policy pilots a single CF2X Crazyflie from a start platform to a landing platform placed somewhere in the world: behind buildings, over mountain ridges, between trees. You never get the pad's exact position: the state vector carries a **noisy search-area centre**, so the drone has to fly to the area, find the pad with its depth camera, and land on it. On some maps the pad is **moving**.
Scoring rewards landing, landing fast, and flying with clearance from obstacles. Autopilot is active with a **0.15 emission allocation** and runs its own [King of the Hill](../king_of_the_hill.md) lineage.
(back to top)
---
## Interface
### Model contract
| | |
|---|---|
| Interface version | `submission_zip.v1` |
| Entry point | `drone_agent.DroneFlightController` |
| Contract file | `swarm_policy_contract.json` (policy_contract.v1) |
| Environment types | city, open, mountain, village, warehouse, forest |
### Observation: `dict` with keys `depth`, `state`
| Key | Dtype | Shape | Range / layout |
|---|---|---|---|
| `depth` | float32 | (128, 128, 1) | [0, 1] |
| `state` | float32 | (141) at evaluation settings | `position_xyz` → `orientation_rpy` → `linear_velocity_xyz` → `angular_velocity_xyz` → `action_history` → `altitude_norm` → `goal_offset_xyz` |
### Action: float32 tensor, shape (5)
| # | Component | Min | Max |
|---|---|---|---|
| 0 | `dir_x` | -1 | 1 |
| 1 | `dir_y` | -1 | 1 |
| 2 | `dir_z` | -1 | 1 |
| 3 | `speed` | 0 | 1 |
| 4 | `yaw` | -1 | 1 |
### Depth camera
The `depth` image comes from a forward-facing camera mounted **0.13 m ahead and 0.05 m above** the drone's centre of gravity. FOV is **90° ± 2°**, seeded per map, so train with a little FOV tolerance. The renderer's depth buffer is converted to metric range, clipped to **[0.5 m, 20 m]**, and normalized: `(d − 0.5) / (20 − 0.5)`. Anything closer than 0.5 m reads 0.0; anything at 20 m or beyond reads 1.0.
### Action semantics
Components 0–2 are a direction vector: it is normalized to a unit vector (an all-zero vector commands zero velocity). The commanded velocity is `3.0 m/s × speed × direction`, with **`SPEED_LIMIT` = 3.0 m/s**. `yaw` maps to a target heading of `yaw × π` radians, slewed at most **3.141 rad/s**. A DSLPID controller converts the command to rotor RPMs each step.
The runner canonicalizes every action before it reaches the sim: components are clipped to the contract bounds and quantized to a fixed step so every validator computes the identical value. A NaN/inf or wrong-shaped action is an artifact fault that fails the seed (`MG_OUTPUT_NONFINITE` / `MG_OUTPUT_CONTRACT`).
(back to top)
---
## State vector layout
`state` is a flat float32 vector of length **141** at evaluation settings (50 Hz control):
| Index | Channel | Units / frame | Notes |
|---|---|---|---|
| 0–2 | Position x, y, z | m, world frame | |
| 3–5 | Orientation roll, pitch, yaw | rad | |
| 6–8 | Linear velocity vx, vy, vz | m/s | |
| 9–11 | Angular velocity wx, wy, wz | rad/s | |
| 12–136 | Action history | raw actions | Last **25** raw 5-dim actions, oldest first; zeroed at every reset (no cross-episode leak) |
| 137 | `altitude_norm` | [0, 1] | Downward single-ray distance ÷ 20 m; reads 1.0 when nothing is hit within 20 m |
| 138–140 | `goal_offset` x, y, z | m | Noisy search-area centre minus drone position (see below) |
### The noisy goal offset
`goal_offset` does **not** point at the pad. It points at a search-area centre drawn once per seed:
- centre = goal + `uniform(−R, +R)` independently on x and y, + `uniform(−5, +5)` on z (clamped to z ≥ 0)
- **R = `search_radius`**, drawn per seed as `min(uniform(5.0, 20.0), feasibility cap)`: the cap guarantees the seed's time budget stays completable within the horizon
So the pad can be anywhere within R metres along each axis of where the offset points (up to about 1.4·R metres diagonally). Getting to the offset is navigation; finding the pad from there is perception. The time budget in [Scoring](#scoring) includes an allowance for sweeping that disk.
(back to top)
---
## Maps
Autopilot runs on **all six environment types** with no exclusions. Each 14-day epoch (anchored Monday 16:00 UTC; 14 days from epoch 19 onward) draws **1,100 seeds**: indices 0–299 are templated by a second, 50-slot template (tiled to 300), indices 300–1099 by the 100-slot benchmark template below. Every submission runs all 1,100.
The benchmark template (the bulk of your score) is 100 interleaved slots:
| Type | Environment | Slots | Start→goal distance | Moving pad | Goal height |
|---|---|---|---|---|---|
| 1 | City | 17 | 22–45 m | 25% | 0.2–1.0 m |
| 2 | Open | 17 | 28–72 m | 80% | 4–14 m |
| 3 | Mountain | 17 | 65–100 m | 25% | terrain surface |
| 4 | Village | 17 | 28–56 m | 25% | ground surface |
| 5 | Warehouse | 16 | 18–35 m | 0% | 0.2–10 m |
| 6 | Forest | 16 | 22–45 m | 0% | 0.2–3 m |
Each type's distance band is split into three equal sub-bands cycled across its slots, so you see the near, middle, and far end of every band. The moving-pad count is `round(slots × probability)`. The template tiles over the 800 benchmark seeds.
### Spawn and goal placement
- **Start**: random XY inside the per-type world box: city ±75 m, open ±60 m, village ±40 m, warehouse ±38 × ±23 m, forest ±42 m, mountain ±(0.6 × 250 m × the map's global scale). On types 1, 2, 5 and 6 the drone spawns on a start platform (radius 0.6 m, height 0.2 m) whose surface sits at a random **0.2–10 m**; type 3 spawns on the terrain surface, type 4 on the ground. The drone starts **0.121 m** above the pad surface, level.
- **Goal**: placed at a random angle from the start, at a radius uniform in the slot's distance band (clamped to the world box, min distance enforced, retried up to 100×). Goal height comes from the slot's band; on mountain and village the pad sits on the terrain/ground and distance is measured in 3D. The world builder may nudge start and goal to a clear spot and reseats the goal height on the landing-pad surface.
(back to top)
---
## Episode rules
| | |
|---|---|
| Drone | 1 × CF2X Crazyflie |
| Control / physics rate | 50 Hz / 50 Hz (`SIM_DT` = 1/50 s) |
| Horizon | 60 s on every type (3,000 control steps max) |
| First observation | From `env.reset()`, before the first `act()` |
### Success: landing, not proximity
Success means **landing on the goal platform** (acceptance radius 0.6 m), held stable for **0.5 s** continuously:
- in contact with a goal-pad body (contact normal force > 0.01), and
- |vz| ≤ 0.5 m/s, and
- horizontal speed relative to the platform ≤ 0.6 m/s, and
- |roll| and |pitch| ≤ 0.26 rad (~15°).
**Moving-platform exception:** the first valid contact is instant success, no dwell required.
A `goal_reach_radius` of 1.0 m and a min-goal-distance figure appear in the per-step info dict; they are diagnostics only. Hovering near the pad scores nothing. Land.
### Failure reasons
| Reason | Trigger |
|---|---|
| `OBSTACLE_COLLISION` | Any contact (normal force > 0.01) with a body that is not the drone or a start/goal platform. Both pads are collision-exempt. |
| `TILT` | \|roll\| or \|pitch\| exceeds 1.047 rad (~60°) |
| `TIMEOUT` | 60 s elapse without a landing |
### Moving platforms
Only Autopilot has them. When a slot is moving, the pad follows a **circular, linear, or figure-8** pattern seeded from the map seed: speed **0.6–1.2 m/s**, orbit radius **2–4 m**, start delay **0–2 s**, smooth transitions of **2.5–3.5 s**. The platform actively steers around obstacles (13 candidate angles) and stays within orbit radius + 0.3 m of its centre. Track it, match its velocity, touch down.
(back to top)
---
## Scoring
Per seed:
```text
final_score = clamp01(0.45 · success_term + 0.45 · time_term + 0.10 · safety_term)
```
| Term | Weight | Definition |
|---|---|---|
| `success_term` | 0.45 | 1.0 on a successful landing, else 0.0 |
| `time_term` | 0.45 | 1.0 if `t ≤ target_time`, else `clamp01(1 − (t − target_time) / (horizon − target_time))` |
| `safety_term` | 0.10 | From the episode's minimum obstacle clearance: 1.0 at ≥ 1.0 m (forest: ≥ 0.6 m), 0.0 at ≤ 0.2 m, linear in between; 0.0 on any collision |
The time budget is search-aware:
```text
target_time = 1.06 × ( dist(start, goal) / 3.0 + 0.75 · π · R² / (5.1 · 3.0) + 2.0 )
```
This is straight-line flight at the 3.0 m/s speed limit, plus a sweep of the search disk (area × 0.75, detection width 5.1 m, at 3.0 m/s), plus 2.0 s to land, all with a 6% buffer. Larger search radii get proportionally more time.
Hard rules:
- **No landing → no score.** A failed seed, or a "success" with a recorded collision, zeroes all terms and pays the participation floor of **0.01** (only when the episode actually ran, t > 0).
- An illegitimate model or an `EVAL_ERROR` seed scores **0.0** flat.
- The clearance scan ignores the drone itself, the ground plane, both pads, and the thin landing-floor body under a low goal pad during final descent: it measures real obstacle proximity, not the landing itself.
To take the family throne, a challenger must beat the champion's benchmark score by at least **0.015** (dynamic floor: 0.005 minimum). See [King of the Hill](../king_of_the_hill.md).
(back to top)
---
## Runtime limits
Your zip runs inside a Docker container as a Cap'n Proto RPC server; the validator calls `reset()` between seeds and `act()` every control step. Autopilot uses the default timing profile:
| Budget | Value |
|---|---|
| First `act()` of a seed | 2.0 s (hardware-fair hard cap: 3.0 reference-seconds) |
| Every later `act()` | 0.6 s baseline-equivalent pure compute (hard cap 2.0 reference-seconds + measured RPC overhead + 50 ms margin) |
| `reset()` between seeds | 5.0 s |
| Connect budget | 60 s to answer the first ping |
| Strikes | 15 timeouts or 3 hard-cap hits fail the seed; each timed-out step substitutes a zero action |
| Zip size | 50 MiB uncompressed max |
| Container | 6 GB RAM, 2 CPUs |
`requirements.txt` packages must be on the Docker whitelist: torch, onnxruntime, stable-baselines3, gymnasium, numpy, opencv and friends; the full list lives in `swarm/constants.py`.
Submissions enter the queue as `PENDING_BENCHMARK` and run the full 1,100-seed benchmark directly. A separate champion-gated screening phase (300 seeds with early-fail checkpoints) exists behind a code constant but is switched off.
(back to top)
---
## Local testing
Package, verify, and benchmark the same artifact with the family pinned explicitly:
```bash
swarm model package --source ./my_model --family-id cf_autopilot
swarm model verify --model Submission/submission.zip
swarm benchmark --model Submission/submission.zip --family-id cf_autopilot --workers 4
```
`--seeds-per-group 1` gives a fast smoke run; see the [CLI reference](../CLI_readme.md) for seed files, timeout relaxation, and report parsing.
(back to top)