A complete walkthrough of the AI Grand Prix Virtual Qualifier 1 interface — every byte the sim sends, every byte we send back, the geometry that ties them together, and the 50-millisecond budget inside which the loop must close.
VQ1 is a downloaded Windows simulator that exposes a drone-racing course over the network. It streams a 640×360 forward-facing camera feed and a stream of telemetry packets; our process consumes both, runs perception and control, and sends four floats back. The interface is small. The geometry is fixed. The timing budget is ~50 ms per closed-loop iteration.
The AIGP team announced the launch for "late next week" (week of May 25, 2026) in their 2026-05-21 email. Roster lock is T−24h before the binary drops.
(throttle, roll, pitch, yaw), four float32s, normalized [−1, 1] except throttle [0, 1].What VQ1 does not give you: GPS, depth, NED position, gate locations. This is by design — the same submission must run on the physical stage where none of those are available either.
Sections marked CONF are confirmed in VADR-TS-002. Sections marked ASSM are reasonable assumptions pending the sim binary drop. Sections marked UNK are flagged unknowns that first connect will resolve.
The sim and the pilot are two processes on the same Windows host. They share nothing but loopback sockets. There is no Python module embedded in the sim, no shared memory, no privileged callback into the renderer — the loop closes entirely through user-space network I/O.
This is the same pattern as PX4 SITL, AirSim, and Gazebo — there is no AIGP-specific SDK. Any language with a UDP socket and a JPEG decoder can implement a pilot. Our reference uses Python, OpenCV, and ultralytics.
The interaction across one render tick of the sim is best read as a UML-style sequence diagram. Wall-clock time flows top to bottom.
The forward camera frame arrives as motion-JPEG over UDP. CONF Geometry is fixed by VADR-TS-002 §3.7–3.8. Packet framing details below are TS-002-derived where possible, otherwise marked.
| Property | Value | Source |
|---|---|---|
| Transport | UDP / IPv4 loopback | TS-002 §4.1 |
| Port | 5600 | TS-002 §4.1 CONF |
| Codec | JPEG (baseline, RGB, 8-bit) | TS-002 §3.8 |
| Resolution | 640 × 360 | TS-002 §3.8 CONF |
| Frame rate (nominal) | ≈30 Hz | TS-002 §3.8 ASSM |
| Datagram framing | 1 JPEG per datagram, <65 KB | UDP MTU limit ASSM |
| Frame metadata | none in-band (use telemetry t_sim for sync) | TS-002 §4.3 UNK |
The pilot binds UDP:5600, receives one datagram per recvfrom(), and
decodes with cv2.imdecode. No GStreamer pipeline, no RTP unpacking,
no reassembly — keep it simple.
# vq1_completion_pilot.py — reference receiver sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2_000_000) sock.bind(('127.0.0.1', 5600)) while running: payload, _ = sock.recvfrom(65535) frame = cv2.imdecode(np.frombuffer(payload, np.uint8), cv2.IMREAD_COLOR) if frame is not None: on_frame(frame) # 640×360 BGR ndarray
A representative annotated frame, showing the detector output, principal point, horizon line, and the 20° upward tilt offset that biases everything in image space toward the lower half of the frame.
The intrinsic matrix is fixed and given in TS-002. The principal point sits at image center; pixels are square. Focal length is identical on both axes.
# Intrinsics — VADR-TS-002 §3.7 K = np.array([ [320.0, 0.0, 320.0], [ 0.0, 320.0, 180.0], [ 0.0, 0.0, 1.0], ]) dist = np.zeros(5) # pinhole, no distortion camera_tilt_rad = math.radians(20.0) # +pitch up
Telemetry is the privileged input we do get. It contains everything an on-board IMU + AHRS would produce — orientation, body angular rates, body linear acceleration — plus a simulator-time stamp for cross-channel synchronization. It does not contain absolute position, velocity, or any course landmarks.
Telemetry dataclass)(w, x, y, z) order. Unit-normalized.(p, q, r) about body X/Y/Z.VADR-TS-002 fixes the field set but not the byte order. The most plausible
packing is naturally-aligned little-endian — 48 bytes total, single datagram.
Confirm against the first recvfrom().
| Property | Value | Notes |
|---|---|---|
| Rate | ≥100 Hz UNK | Likely 100–500 Hz. Confirmed on first connect. |
| Ordering guarantee | monotonic t_sim | Drop packets with non-increasing t_sim. |
| Loss tolerance | graceful | UDP — assume up to ~1% packet loss; never block on it. |
| Coordinate convention | body NED ASSM | X-forward, Y-right, Z-down. Confirmed by first-flight inspection of accel under hover. |
What we derive from it:
attitude_q as a setpoint reference between gate sightings.body_rates and body_accel for up to 30 frames.t_sim drives the PID integral term, not wall-clock.Three frames are in play. Getting these consistent is the single most common source of sign-flip bugs in a racing stack, so the convention is fixed here and referenced from every PnP and control call.
| Transform | Source | Static / dynamic | Inverse needed? |
|---|---|---|---|
| R_world←body | attitude_q (telemetry) | dynamic, ≥100 Hz | Yes — for gravity-down vector |
| R_body←cam | fixed 20° pitch up | static (compile-time) | Yes — PnP output is in cam frame |
| T_world←cam | composed | dynamic | Never needed in VQ1 |
Two gate sizes exist in the AIGP catalog. VQ1 uses the smaller of the two exclusively. CONF
| Gate | Inner span | Material | VQ1 | VQ2 |
|---|---|---|---|---|
| Standard square | 1.5 m × 1.5 m | painted PVC | ✓ | ✓ |
| Wide gate | 2.7 m × 2.7 m | painted PVC | — | ✓ |
The detector emits four keypoints per gate, ordered TL → TR → BR → BL (image
space). Combined with known metric corner coordinates and the camera intrinsic
matrix, cv2.solvePnP with the IPPE square solver returns the gate's
SE(3) pose in the camera frame.
Four floats, sent back to the sim. The body of the entire control surface is this small.
0.0 (off) to 1.0 (max). Hover ≈ 0.55 at the SimDrone weight.−1.0 (max left) to +1.0 (max right). Body X axis.−1.0 (max nose-down) to +1.0 (max nose-up). Body Y axis.−1.0 (max CCW) to +1.0 (max CW). Body Z axis.Throttle is the only non-symmetric axis. The thrust map is roughly affine in the operational range, but the hover point sits well above 0.5 — this is the single quirk most likely to bite a naïve controller.
Send one command per inbound frame — the pilot is frame-driven, not clock-driven. If the detector loses sight, the pilot still emits a fresh command derived from propagated state; never let the control channel go silent.
# vq1_completion_pilot.py:ControlCmd → wire @dataclass class ControlCmd: throttle: float # [0, 1] roll: float # [-1, +1] pitch: float # [-1, +1] yaw: float # [-1, +1] def send(cmd: ControlCmd): payload = struct.pack('<ffff', cmd.throttle, cmd.roll, cmd.pitch, cmd.yaw) sock.sendto(payload, sim_addr) # <= 16 bytes
The reference SimDrone hovers at throttle = 0.55 (not 0.5), implying the model uses
2·m·g max thrust rather than 1·m·g. Max usable pitch is around 15° before lift collapses.
Conservative VQ1 gains stay well inside this envelope.
The pilot must complete photon-to-PWM in under 50 ms to track a 6 m/s drone through a 1.5 m gate with margin. Measured per-stage on the training rig (RTX 5080, i9, Windows), the budget breaks down as follows:
| Stage | Typical | P99 | Hot path | Notes |
|---|---|---|---|---|
| recv() + cv2.imdecode | 5 ms | 9 ms | CPU | libjpeg-turbo via OpenCV |
| YOLO11n forward pass | 8 ms | 12 ms | GPU | FP16 TensorRT engine, batch 1 |
| Keypoint extraction | 1 ms | 2 ms | CPU | argmax over heads |
| cv2.solvePnP (IPPE_SQUARE) | 1 ms | 1 ms | CPU | closed-form, no RANSAC |
| Frame transforms + PID | <1 ms | 1 ms | CPU | numpy quaternion math |
| sendto() to sim | <1 ms | 1 ms | kernel | 16-byte payload |
| Total per frame | ≈17 ms | ≈26 ms | ~33 ms headroom to cap |
VQ1 doesn't need a planner — the next gate is whichever gate is in front of us. The state machine is correspondingly small: two states and one timer.
| State | Action | Exit condition |
|---|---|---|
| TRACK | PnP yields gate pose → three PIDs (heading, altitude, pitch) → forward thrust | 30 consecutive frames with no detection above confidence 0.5 |
| SEARCH | Hover at last commanded altitude, slow yaw rate 0.3 rad/s, throttle held at hover | Any single detection with confidence >0.5 |
VADR-TS-002 pins almost the entire interface, but three details are not real until the binary is in hand. The pilot is structured so that all three are local edits, not redesigns.
| # | Unknown | Where to look | Code site |
|---|---|---|---|
| 1 | Telemetry packet byte order & rate | First recvfrom() on telemetry port; print(len(payload)) | StubSimClient.next_frame() ↓ |
| 2 | Sim's control input port | AIGP setup README in the download | StubSimClient.send() ↓ |
| 3 | Submission packaging format | AIGP submission instructions email | submit_check.py |
All three resolve in well under a day of work, in parallel. Schemas and the PID-driven control loop are already wired; the stub only needs three real implementations to replace three stubbed methods. The contracts in this document become the unit tests.