# Interceptor
Air-to-air pursuit: find a fleeing drone with your depth camera and take it down.
Your drone lifts off from a pad, crosses 60–100 m of open terrain to a search area, and hunts a second drone flown by the validator. The target cruises until you get close, then flees and jinks. You win by physically catching it (ram it or close within the kill radius) before the 60-second clock runs out. Both aircraft are the 36 cm interceptor airframe (a cf2x scaled ×3), big enough to show up on the depth sensor at range.
This family is completed: the score reached the 0.999 solve threshold, the crown is frozen, and the winning solution is preserved as open source. Its effective payout is 0%; the 30% allocation is historical.
---
Table of Contents
- At a Glance
- Model I/O
- State Vector Layout
- Maps
- Episode Rules
- Scoring
- Local Testing
---
## At a Glance
| | |
|---|---|
| Family ID | `cf_interceptor` |
| Miner-controlled drones | 1 (a second, validator-flown target drone shares the world) |
| Map | Open terrain only, 180 m extent |
| Control rate | 50 Hz (dt = 1/50 s) |
| Episode horizon | 60 s (3,000 control steps) |
| Speed cap | **6.0 m/s** (vs 3.0 m/s in other families) |
| Airframe | 36 cm interceptor drone: mass 0.729 kg, thrust-to-weight 2.25, max 60 km/h |
| Catch condition | Contact with the target, or distance ≤ 0.15 m |
| Score weights | 0.5 success + 0.5 time (no safety term) |
| Emission allocation | 0% effective payout (historical 0.30) |
(back to top)
---
## Model I/O
### Model contract
| | |
|---|---|
| Interface version | `submission_zip.v1` |
| Entry point | `drone_agent.DroneFlightController` |
| Contract file | `swarm_policy_contract.json` (policy_contract.v1) |
| Environment types | open |
### Observation: `dict` with keys `depth`, `state`
| Key | Dtype | Shape | Range / layout |
|---|---|---|---|
| `depth` | float32 | (1024, 1024, 1) | [0, 1] |
| `state` | float32 | (140) at evaluation settings | `position_xyz` → `orientation_rpy` → `linear_velocity_xyz` → `angular_velocity_xyz` → `action_history` → `altitude_norm` → `search_clue_offset_xy` |
### 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 |
> There is **no `rgb` key and no 6th action component** here: those are Search-and-Rescue features. Do not copy SAR shapes for this family.
### Depth camera
The forward depth camera is the long-range HD variant, tuned so a 36 cm drone is resolvable at chase distances:
| Property | Value |
|---|---|
| Resolution | 1024 × 1024 × 1 |
| Working range | 0.5 m – **100 m** (other families clip at 20 m) |
| Normalization | PyBullet depth linearized with near = 0.05 m, far = 110 m, clipped to [0.5, 100] m, mapped to [0, 1] via `(d − 0.5) / (100 − 0.5)` |
| Field of view | 90° ± 2°, randomized per seed |
| Lighting | Light direction randomized per seed |
### Action semantics
Actions run through the VEL controller: commanded velocity = `6.0 × |speed| × unit(dir_xyz)`, so `speed` is a fraction of the 6.0 m/s family cap. The `yaw` component is an absolute target yaw (`clip(yaw, −1, 1) × π`), rate-limited to 3.141 rad/s per control step; a PID converts the command to rotor RPMs.
Each control step has a guaranteed baseline-equivalent pure-compute budget of **0.600 s**. The normal per-action liveness cap is **2.0 reference-seconds** plus measured RPC overhead and a 50 ms margin. Separate wall-clock limits allow a 2.0 s compute budget with a 3.0 reference-second hard cap for the first step and 5.0 s for `reset()`. Up to 15 slow-action strikes are tolerated per seed before the episode is scrapped.
(back to top)
---
## State Vector Layout
`obs["state"]` is a flat float32 vector of length **140** at evaluation settings (50 Hz control):
| Index | Channel | Units / frame | Notes |
|---|---|---|---|
| 0–2 | `position_xyz` | m, world frame | Unbounded |
| 3–5 | `orientation_rpy` | rad | Roll, pitch, yaw |
| 6–8 | `linear_velocity_xyz` | m/s | |
| 9–11 | `angular_velocity_xyz` | rad/s | |
| 12–136 | `action_history` | n/a | Last 25 actions × 5 floats = 125 values; zeroed at reset (no leakage across episodes) |
| 137 | `altitude_norm` | n/a | Single downward ray ÷ 20 m; ray origin 0.27 m below drone center; reads 1.0 when nothing is hit within 20 m |
| 138–139 | `search_clue_offset_xy` | m | (Δx, Δy) from your position to a coarse hint near the target (see below) |
### The search clue
The clue is a deterministic hint point sampled **within `search_radius` of the target's current position**, never the target itself. Per episode, `search_radius` is drawn uniform in **[10, 40] m**, and the hint is re-sampled every **2.0 s** so it tracks the moving target. All of it derives from the map seed, so episodes replay identically. The radius value itself is not in the observation. Your policy has to close the last stretch on the depth camera alone.
(back to top)
---
## Maps
Interceptor runs **only on open maps** (challenge type 2). Task generation forces the open type, and the terrain extent is enlarged to **180 m** so a full-speed chase fits.
The benchmark template is 100 slots, all open, cycling three start-distance bands with no moving platforms:
| Band | Chase gap |
|---|---|
| 1 | 60.0 – 73.3 m |
| 2 | 73.3 – 86.7 m |
| 3 | 86.7 – 100.0 m |
The template repeats across the seed run (the backend's benchmark defaults to 1,100 seeds per run, env-overridable). Every submission goes straight to the full benchmark: screening is disabled (`SCREENING_ENABLED = False`); a dormant 8-slot screening template and its thresholds exist behind that constant, but they do not run today.
### Spawn geometry (per seed)
- Chase gap uniform in **[60, 100] m** at a random bearing; the chase midpoint is jittered ±10 m per axis, and chaser start / target position land on opposite sides of it.
- **Chaser** spawns resting on a start pad (radius 1.8 m, height 0.6 m) with a 0.363 m takeoff buffer above the pad top. The pad is collision-exempt for you.
- **Target** spawns airborne at ground + uniform **[3, 25] m**, with the ground level resolved by surface raycast.
(back to top)
---
## Episode Rules
Simulation runs at **50 Hz** with a hard horizon of **60 s** (3,000 steps).
### How the target flies
The validator flies the target with a PID controller every control step. It is deterministic per seed: same seed, same chase.
| Behavior | Trigger | Detail |
|---|---|---|
| **Cruise** | Default | 2.7 m/s (0.45 × 6.0) on a slowly-varying, seed-picked heading |
| **Flee** | Chaser within **12 m** | 4.5 m/s (0.75 × 6.0) directly away, plus a sinusoidal lateral jink (gain 0.6, frequency seed-picked in 0.3–1.0 Hz, random phase) |
| **Patrol leash** | > 55 m from spawn XY | Heads home at cruise speed |
| **Vertical band** | Always | Held between ground + 3 m and ground + 25 m (vz = ±0.5 × speed); the ground level is tracked live by a downward ray, so the band follows terrain |
### Termination
| Outcome | Condition |
|---|---|
| **Success (catch)** | Physical contact between chaser and target (ram), or chaser–target distance ≤ **0.15 m** (anti-tunnel guard). The target is exempt from your obstacle-collision check: ramming it is a catch, not a crash. |
| `OBSTACLE_COLLISION` | Any chaser contact (normal force > 0.01) with a non-exempt body. Exempt: your own pad, the target, your own body. |
| `TILT` | \|roll\| or \|pitch\| > **75°** (family override; the default elsewhere is 60°). |
| `TIMEOUT` | 60 s elapsed without a catch. |
| `INFEASIBLE` | The fleeing target crashes itself into the world (contact force > 3.0 N, excluding you) before a catch. Not your fault: scores participation. |
Docker runtime profile: image `base`, resource class `navigation`, `sar_mode: false`; the container sees `SWARM_CHALLENGE_FAMILY_ID=cf_interceptor` and `SWARM_RUNTIME_PROFILE=interceptor`.
(back to top)
---
## Scoring
```text
score = 0.5 × success + 0.5 × time
```
There is **no safety term** in this family: closing distance aggressively is the point.
| Case | Score |
|---|---|
| Clean catch | `clamp(0.5 + 0.5 × time_term)` in [0, 1] |
| Catch, but collision flagged | 0.01 |
| No catch (collision, infeasible, spawn failure, tilt, timeout) | 0.01 participation |
| Evaluator error or illegitimate model | 0.0 |
### Time term
```text
time_term = 1.0 if t ≤ target_time
= 1 − (t − target_time) / (60 − target_time) otherwise, clamped to [0, 1]
```
where `t` is the catch time and the par (`target_time`) scales with the seed's chase gap:
```text
target_time = min(1.1 × (gap / 3.75 + 10.0), 0.95 × 60)
```
`gap` is the 3-D start-to-target distance, **3.75 m/s** is the assumed net closing speed (your 6.0 m/s cap minus the target's flee margin: `6.0 × (1 − 0.75 × 0.5)`), 10.0 s is acquisition slack, and 1.1 is the buffer. An 80 m gap gives a par around 34.5 s; the cap is 57 s. Catch at or under par for a full time score; every second past par bleeds it linearly toward zero at the horizon.
Per-episode `info` exposes the chase telemetry you'll want when tuning: `intercept_caught`, `target_crashed`, `intercept_min_dist`, `kill_radius_m`, `search_radius_m`, and `t_to_intercept`.
(back to top)
---
## Local Testing
This family is completed and no longer accepts new submissions. New submissions should target [Office Interceptor](office_interceptor.md).
(back to top)