Find My Force

RF Signal Classification & Geolocation Challenge

Checking...
Authenticate Train on Live Feed Classify Eval Set Get Scored

Quick Start

1

Check server health

GET /health No auth required
2

Get receiver positions & path loss model (once)

GET /config/receivers   GET /config/pathloss
3

Connect to the observation feed

GET /feed/stream (SSE) or GET /feed/observations (polling)
4

Build your classifier using the live feed as training data

POST /submissions/classify or POST /submissions/batch (practice, not scored)
5

Fetch the evaluation dataset and submit for official scoring

GET /evaluate/observations   POST /evaluate/submit (opens ~1 hr before hacking ends)
6

Check your score

GET /scores/me

Authentication

All endpoints (except /health) require your team API key in the request header:

X-API-Key: <your-team-api-key>

Your API key will be provided by the organizer at the start of the event.

401 Missing header 403 Invalid key 429 Rate limit exceeded

API Endpoints

GET /health No auth

Check if the server is running and get simulation state.

Response

{
  "status": "ok",
  "simulation_state": "running",  // "stopped" | "running" | "paused"
  "evaluation_open": false,       // true when eval endpoints are unlocked
  "timestamp": "2026-03-07T09:00:00+00:00"
}
GET /feed/stream Server-Sent Events

Real-time streaming of RF observations via SSE. Recommended for most teams.

Event Types

  • observation — Contains an observation JSON object
  • keepalive — Empty, sent every 30s if no observations

Observation Schema

{
  "observation_id": "obs-a1b2c3",        // Unique ID, use this when submitting
  "timestamp": "2026-03-07T09:05:12Z",
  "receiver_id": "RX-01",               // Which receiver detected this
  "rssi_dbm": -72.4,                    // Received signal strength (dBm)
  "iq_snapshot": [0.12, -0.34, ...],    // 256 floats: [0..127]=I, [128..255]=Q
  "snr_estimate_db": 14.2,              // Signal-to-noise ratio (dB)
  "time_of_arrival_ns": 1523.7          // Nanoseconds (may be null)
}
Note: If you get a 503 error, too many SSE connections are open. Fall back to the polling endpoint.
GET /feed/observations Polling alternative

Poll for recent observations. Use if SSE is not suitable for your setup.

Query Parameters

ParamTypeDefaultDescription
sincestringnoneISO 8601 timestamp. Only return observations after this time.
limitint50Max observations to return (1–500)
receiver_idstringnoneFilter by specific receiver

Response

{
  "observations": [ ... ],  // Array of observation objects (same schema as SSE)
  "count": 50,
  "has_more": true          // true if more observations exist beyond limit
}
POST /submissions/classify Single submission

Submit a classification for one observation. Optionally include a position estimate for geolocation scoring.

Request Body

{
  "observation_id": "obs-a1b2c3",         // required, from the feed
  "classification_label": "Radar-Altimeter", // required, your predicted signal type
  "confidence": 0.85,                     // required, 0.0 to 1.0
  "estimated_latitude": 49.2608,          // optional, for geolocation scoring
  "estimated_longitude": -123.2460        // optional, for geolocation scoring
}

Response

{
  "submission_id": "uuid-string",
  "observation_id": "obs-a1b2c3",
  "accepted": true,
  "message": "accepted"      // or "duplicate" / "observation_not_found"
}
POST /submissions/batch Batch (1–100)

Submit up to 100 classifications in one request.

Request Body

{
  "submissions": [
    {
      "observation_id": "obs-a1b2c3",
      "classification_label": "Radar-Altimeter",
      "confidence": 0.85,
      "estimated_latitude": 49.2608,      // optional
      "estimated_longitude": -123.2460    // optional
    },
    ...  // 1 to 100 items
  ]
}

Response

{
  "results": [ ... ],       // Array of per-submission results
  "accepted_count": 8,
  "rejected_count": 2
}
GET /evaluate/observations EVALUATION Fixed test set

Get the evaluation dataset — a fixed set of observations that are the same for every team. This is the data your final score is based on.

Time-gated: This endpoint is locked until approximately 1 hour before hacking ends. You will receive a 403 until the organizers open evaluation. Use the live feed to train and iterate until then.
Important: The eval dataset is separate from the live feed. It is deterministic — every team gets identical observations. Your score depends on how well you classify ALL of these.

Response

{
  "observations": [
    {
      "observation_id": "eval-uuid-...",
      "timestamp": "2026-03-07T00:00:00+00:00",
      "receiver_id": "RX-01",
      "rssi_dbm": -72.4,
      "iq_snapshot": [0.12, -0.34, ...],  // 256 floats
      "snr_estimate_db": 14.2,
      "time_of_arrival_ns": 1523.7
    },
    ...
  ],
  "count": 75   // Total eval observations (~50-80)
}
POST /evaluate/submit EVALUATION Submit & get scored

Submit your classifications for the eval observations and receive your score immediately. Observations you don't submit count as wrong.

Time-gated: Submissions are locked until the organizers open evaluation (~1 hour before hacking ends). Returns 403 until then.
60-second cooldown between evaluation attempts. Your best score across all attempts is tracked.

Request Body

{
  "submissions": [
    {
      "observation_id": "eval-uuid-...",       // from /evaluate/observations
      "classification_label": "Radar-Altimeter",
      "confidence": 0.92,
      "estimated_latitude": 49.2608,           // optional
      "estimated_longitude": -123.2460         // optional
    },
    ...  // Submit for ALL eval observations for best score
  ]
}

Response

{
  "total_score": 67.4,
  "classification_score": 72.1,
  "geolocation_score": 58.3,
  "novelty_detection_score": 65.0,
  "total_observations": 75,
  "submissions_count": 70,
  "correct_classifications": 52,
  "coverage": 93.3,                 // % of eval obs you submitted for
  "average_cep_meters": 312.5,
  "per_class_scores": [ ... ],
  "attempt_number": 3,
  "is_best": true,                  // Whether this attempt beat your previous best
  "best_total_score": 67.4,
  "cooldown_seconds": 60
}
GET /scores/me Your team's score

Get your team's detailed score breakdown.

Response

{
  "team_id": "abc123",
  "team_name": "Signal Hunters",
  "total_score": 67.4,
  "classification_score": 72.1,       // F1-based (40% of total)
  "geolocation_score": 58.3,          // CEP-based (30% of total)
  "novelty_detection_score": 65.0,    // Hostile detection (30% of total)
  "submissions_count": 142,
  "correct_classifications": 98,
  "average_cep_meters": 312.5,        // null if no geo submissions
  "per_class_scores": [
    { "label": "Radar-Altimeter", "precision": 0.92, "recall": 0.88, "f1": 0.90, "count": 45 },
    ...
  ],
  "last_submission_at": "2026-03-07T10:30:00Z"
}
GET /config/receivers Receiver network

Get positions and specifications of all receivers. Call once at startup — needed for geolocation.

Response

{
  "receivers": [
    {
      "receiver_id": "RX-01",
      "latitude": 49.2606,
      "longitude": -123.2460,
      "sensitivity_dbm": -90.0,       // Min detectable signal (dBm)
      "timing_accuracy_ns": 50.0      // ToA measurement noise (ns)
    },
    ...
  ]
}
GET /config/pathloss RF propagation model

Get the path loss model parameters. Use to convert RSSI measurements to distance estimates.

Response

{
  "rssi_ref_dbm": -30.0,             // RSSI at reference distance
  "d_ref_m": 1.0,                    // Reference distance (meters)
  "path_loss_exponent": 2.8,         // Environment-dependent (2=free space, 3+=urban)
  "rssi_noise_std_db": 3.5           // Measurement noise std dev (dB)
}

Path Loss Formula

RSSI = rssi_ref - 10 × n × log10(d / d_ref)

Rearranged for distance: d = d_ref × 10((rssi_ref - RSSI) / (10 × n))

Data Format

Training Data (Labeled Friendly Signals)

HDF5 file containing labeled IQ waveforms for all friendly signal types across SNR levels.

Download →

IQ Snapshot

  • 256 float values per observation
  • Indices 0–127 = In-phase (I) component
  • Indices 128–255 = Quadrature (Q) component
  • Sample rate: 10 MS/s
  • To get complex IQ: complex_iq = I + j*Q

Key Fields

FieldTypeDescription
rssi_dbmfloatReceived Signal Strength Indicator in dBm. Stronger (less negative) = closer or more powerful emitter.
snr_estimate_dbfloatSignal-to-Noise Ratio in dB. Higher = cleaner signal, easier to classify.
time_of_arrival_nsfloat|nullTime of arrival in nanoseconds. Can be used with multiple receivers for TDOA geolocation. May be null.
receiver_idstringWhich receiver detected this observation. Same emitter signal detected by different receivers will have different observation IDs.

Signal Catalog

The simulation contains signals from three affiliations. Your training data (the live feed) includes labeled friendly signals only. Hostile and civilian signals appear in the evaluation set but you have no labeled training examples for them.

Friendly (in training data)

Signal TypeModulationOperational Role
Radar-AltimeterFMCWNavigation radar on friendly UAVs
SatcomBPSKSatellite communications link
short-rangeASKLow-power telemetry from friendly UGVs

Hostile (from intelligence reports — NOT in training data)

Intelligence briefing: Adversary forces are operating these systems in the area. You have no labeled training examples — your system must detect them as out-of-distribution and classify them.
Signal TypeModulationAssessment
Airborne-detectionPulsedAirborne surveillance radar
Airborne-rangePulsedAirborne range-finding radar
Air-Ground-MTIPulsedAir-to-ground moving target indicator radar
EW-JammerJammingElectronic warfare / broadband jammer

Civilian (NOT in training data)

Signal TypeModulationDescription
AM radioAM-DSBCommercial AM radio broadcast
Use these exact labels when submitting classifications via /evaluate/submit. Labels are case-insensitive. For example, "Radar-Altimeter" and "radar-altimeter" both work. Misclassifying a hostile signal as a friendly type means you miss the novelty detection points for that observation.

Scoring

Total Score = 40% Classification + 30% Geolocation + 30% Novelty

Classification (40%)

Macro-averaged F1 score across all signal classes. Precision and recall weighted equally per class. Includes both friendly and non-friendly types.

Geolocation (30%)

Exponential decay scoring based on position error. Scale = 200m. Closer estimates score higher. Requires estimated_latitude and estimated_longitude in submissions.

Novelty Detection (30%)

Can you spot signals that are NOT in the training data? 60% for correctly flagging non-friendly signals (any label that isn't a friendly type), 40% for identifying the specific hostile/civilian type from the catalog above.

How scoring works: Your official score comes from the evaluation dataset (/evaluate/observations/evaluate/submit). The live feed is for training and practice only. The eval set is fixed — every team gets the same observations. Any observation you don't submit counts as wrong, so aim for full coverage.
Tip: The training data only includes friendly signal types. Hostile and civilian signals are NOT in your labeled dataset. Your system needs to detect these unknown signals (novelty detection) and classify them using the hostile signal catalog from the intelligence briefing above.

Code Examples

Python — Connect to SSE stream
import json
import requests

API_URL = "http://<server-address>:8000"
API_KEY = "<your-team-api-key>"

# Stream observations via SSE
resp = requests.get(
    f"{API_URL}/feed/stream",
    headers={"X-API-Key": API_KEY},
    stream=True
)

for line in resp.iter_lines(decode_unicode=True):
    if line.startswith("data: "):
        data = line[6:]
        if data:
            obs = json.loads(data)
            print(f"Observation {obs['observation_id']} from {obs['receiver_id']}")
            # Process obs['iq_snapshot'] with your ML model here
Python — Submit a classification
import requests

resp = requests.post(
    f"{API_URL}/submissions/classify",
    headers={
        "X-API-Key": API_KEY,
        "Content-Type": "application/json"
    },
    json={
        "observation_id": "obs-a1b2c3",
        "classification_label": "Radar-Altimeter",
        "confidence": 0.92,
        "estimated_latitude": 49.2608,
        "estimated_longitude": -123.2460
    }
)

result = resp.json()
print(f"Accepted: {result['accepted']}, Message: {result['message']}")
Python — Evaluation flow (get scored)
import requests

API_URL = "http://<server-address>:8000"
API_KEY = "<your-team-api-key>"
headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# 1. Fetch the eval dataset (same for every team)
eval_resp = requests.get(f"{API_URL}/evaluate/observations", headers=headers)
eval_obs = eval_resp.json()["observations"]
print(f"Got {len(eval_obs)} eval observations")

# 2. Classify each observation with your model
submissions = []
for obs in eval_obs:
    label = your_model.predict(obs["iq_snapshot"])   # Your ML model
    lat, lon = your_model.geolocate(obs)              # Optional geolocation
    submissions.append({
        "observation_id": obs["observation_id"],
        "classification_label": label,
        "confidence": 0.9,
        "estimated_latitude": lat,
        "estimated_longitude": lon,
    })

# 3. Submit and get your score immediately
score_resp = requests.post(
    f"{API_URL}/evaluate/submit",
    headers=headers,
    json={"submissions": submissions}
)
result = score_resp.json()
print(f"Score: {result['total_score']:.1f} (Best: {result['best_total_score']:.1f})")
print(f"Coverage: {result['coverage']}% | Attempt #{result['attempt_number']}")
curl — Quick reference
# Health check (no auth)
curl http://<server>:8000/health

# Get receivers
curl -H "X-API-Key: YOUR_KEY" http://<server>:8000/config/receivers

# Poll observations
curl -H "X-API-Key: YOUR_KEY" "http://<server>:8000/feed/observations?limit=10"

# Submit classification
curl -X POST http://<server>:8000/submissions/classify \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"observation_id":"obs-1","classification_label":"Radar-Altimeter","confidence":0.9}'

# Check your scores
curl -H "X-API-Key: YOUR_KEY" http://<server>:8000/scores/me

Important Notes

  • Use the exact observation_id from the feed when submitting. Each observation is unique per receiver.
  • The server does NOT group observations by emitter — the same emitter signal detected by multiple receivers produces multiple observations. Associating them is part of the challenge.
  • No ground truth is exposed in the feed. You will not know the true signal type or emitter location during the event.
  • Duplicate submissions for the same observation are rejected. Submit once per observation.
  • time_of_arrival_ns may be null — handle this gracefully.
  • Rate limits apply per team. If you get 429, back off and retry after a few seconds.
  • The training data only contains friendly signal types. Hostile signals use different modulation profiles not in the training set.
  • Your official score comes from the evaluation endpoint (/evaluate/submit), not from the live feed submissions. The live feed is for training.
  • The eval dataset is fixed. Submit classifications for all eval observations — missing ones count as wrong. You can re-submit every 60 seconds; your best score is kept.