RF Classification · Geolocation · Tactical Awareness
"What's nearby? Is it friendly? When was it last seen?"
Every design decision in this system exists to answer those three questions faster and with more confidence than the operator could alone.
In contested electromagnetic environments, a signals intelligence operator is drowning in data. A network of receivers is reporting hundreds of RF detections per minute — each one a raw 256-element IQ snapshot with a signal strength reading and a timestamp. Somewhere in that stream are friendly UAVs, ground vehicles, and sensor networks. Also in that stream are adversary radars and communications systems that the operator has never seen before. And civilian devices. All interleaved. All unlabeled.
The operator doesn't have time to run an FFT. They need a map, color-coded, with confidence indicators, updating in real time. They need Find My Force.
This is not a supervised classification problem.
We have labeled training data for six friendly signal types. We have zero labeled examples of hostile emitters. The guide is explicit: "Intelligence suggests adversary forces are operating radar and communications systems in the area, but you don't know exactly what they look like. Your system must figure that out."
A team that treats this as a six-class classification task will correctly label every friendly signal and miss every hostile emitter entirely. That's not a signals intelligence system — it's a friendly-force tracker with a blind spot the size of the threat.
Our system is built around a semi-supervised pipeline. We learn what friendly looks like, and we detect what doesn't fit. Then we go further: we cluster the anomalies to distinguish between hostile subtypes, giving the operator not just "unknown" but "Unknown-Alpha" and "Unknown-Bravo" — distinct threat categories discovered from the data itself.
┌─────────────────────────────────────────────────────────────┐
│ SIMULATION FEED │
│ (flat stream of independent receiver observations) │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SIGNAL CLASSIFIER (ML PIPELINE) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Feature │ │ Supervised │ │ Anomaly Detection │ │
│ │ Extraction │──│ Classifier │──│ (Autoencoder + │ │
│ │ (25+ feats) │ │ (Ensemble) │ │ OOD Scoring) │ │
│ └──────────────┘ └──────────────┘ └────────────────────┘ │
│ │ │
│ ┌────────▼─────────┐ │
│ │ Hostile Subtype │ │
│ │ Clustering │ │
│ │ (DBSCAN) │ │
│ └──────────────────┘ │
│ │
│ Output: label, confidence, is_friendly, is_ood, embedding │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ OBSERVATION ASSOCIATION ENGINE │
│ │
│ Three-criteria grouping: │
│ 1. Temporal proximity (configurable window) │
│ 2. Signal similarity (classification + embedding cosine) │
│ 3. RSSI geometric consistency (single-source hypothesis) │
│ │
│ Output: grouped observations ready for geolocation │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ GEOLOCATION ENGINE │
│ │
│ RSSI → distance (path-loss model) │
│ Weighted nonlinear least squares (scipy.optimize) │
│ Confidence gates: bad fit → inflated uncertainty │
│ Coordinate conversion: lat/lon ↔ local ENU │
│ │
│ Output: lat/lon, uncertainty radius, contributing receivers │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ TRACK MANAGER │
│ │
│ Create / Update / Age tracks │
│ Position: Kalman filter (constant-velocity model, numpy) │
│ Classification: rolling majority vote weighted by conf. │
│ Staleness: fresh (<10s) → stale (10-60s) → lost (>60s) │
│ Platform correlation: co-located emitters → single entity │
│ │
│ Output: persistent tracks with full history │
└──────────────────────────┬──────────────────────────────────┘
│
WebSocket
│
▼
┌─────────────────────────────────────────────────────────────┐
│ COMMON OPERATING PICTURE (COP) │
│ │
│ Mapbox GL JS + OpenStreetMap │
│ Color-coded markers (friendly / hostile / unknown / civ.) │
│ Uncertainty rings scaled to CEP │
│ Staleness visualization (solid → faded → ghost) │
│ Click-to-detail panel with full track metadata │
│ Hostile alert banner │
│ CoT/ATAK XML export │
└─────────────────────────────────────────────────────────────┘
| Requirement | Our Implementation |
|---|---|
| Classify known friendly types | Ensemble classifier: XGBoost on 25+ engineered features combined with a 1D CNN on raw IQ. The ensemble hedges risk — tree-based models are interpretable and strong on tabular features; the CNN captures temporal patterns that hand-crafted features miss. |
| Detect out-of-distribution signals | Three-layer anomaly detection: (1) softmax confidence threshold as a safety net, (2) autoencoder trained exclusively on friendly signals — high reconstruction error flags OOD, (3) embedding-space distance via one-class SVM for corroboration. |
| Distinguish hostile subtypes | DBSCAN clustering on anomalous embeddings. We don't just flag "unknown" — we discover and label coherent hostile categories (Unknown-Alpha, Unknown-Bravo) from the data itself. Distinct colors on the COP per subtype. |
| Handle low SNR | Below 0 dB, we shift weight toward frequency-domain features (spectral centroid, spectral flatness, Welch PSD, band energy ratios) which are empirically more noise-robust than time-domain features. |
| Calibrated confidence | Temperature scaling on a held-out validation set. A reported 80% confidence means the model is correct approximately 80% of the time — operationally meaningful, not just raw softmax. |
Feature engineering spans three domains. Time-domain: amplitude envelope statistics (mean, std, kurtosis, crest factor), zero-crossing rate, duty cycle, I/Q energy ratio. Phase/frequency: instantaneous frequency statistics, circular variance, phase wrap count. Frequency-domain: FFT spectral centroid, bandwidth, flatness, entropy, band energy ratios, significant peak count. These were selected to discriminate between the six modulation types in the training data (FMCW, BPSK, GFSK, DSSS-OQPSK, DSSS-CCK, ASK) with explicit attention to the confusion pairs that matter most: Zigbee vs. Wi-Fi (both DSSS) and any signal vs. noise at extreme low SNR.
| Requirement | Our Implementation |
|---|---|
| Position estimation | Nonlinear least squares trilateration via scipy.optimize.least_squares. RSSI converted to distance using the provided path-loss model. Coordinate math done in a local ENU frame to avoid lat/lon distortion. |
| Weighted estimation | Per-receiver weights based on inverse RSSI noise variance and classification confidence. Closer, higher-confidence receivers contribute more. |
| Uncertainty estimation | Residual-based CEP: the fit quality directly maps to the uncertainty ring radius on the COP. Confidence gates: if the fit residual exceeds 2× the RSSI noise standard deviation, uncertainty is inflated and the fix is flagged as degraded. |
| Fewer than 3 receivers | Graceful degradation. A 2-receiver group produces a low-confidence fix; a 1-receiver group preserves the track at last-known position with a "degraded" indicator. The system never crashes or drops a track because of sparse data. |
| Moving targets | Kalman filter with a constant-velocity motion model (numpy). The filter estimates position and velocity jointly, predicts between updates for accurate track matching, and fuses each new fix weighted by measurement uncertainty. EMA fallback if Kalman state is unavailable. |
This is where half the competition will lose. A perfect classifier behind a mediocre map is operationally useless. Our COP is designed so that an operator can answer all three core questions — What's nearby? Is it friendly? When was it last seen? — within five seconds of looking at the screen.
| Element | Design Rationale |
|---|---|
| Color coding | Green (friendly), red (hostile), orange (unknown/unverified), blue (civilian). Immediate visual parsing with no legend lookup required after first glance. Hostile subtypes get distinct red/orange shades. |
| Uncertainty rings | Semi-transparent circles proportional to the estimated CEP. A tight ring means high confidence. A wide ring means the operator should treat the position as approximate. This is more than cosmetic — it communicates the quality of the intelligence. |
| Staleness | Fresh tracks are solid and opaque. Stale tracks (10–60s since last observation) fade. Lost tracks (>60s) render as grey ghosts with dashed outlines. The operator can instantly distinguish live intelligence from aged data. |
| Detail panel | Click any track: signal type, modulation, confidence %, lat/lon, uncertainty radius, observation count, contributing receiver IDs, last-seen timestamp, full position history. Everything the operator needs to assess the track without switching tools. |
| Alert banner | When a new hostile track is created, a banner flashes at the top of the COP. The most operationally critical event — a new threat — is impossible to miss. |
| Visual hierarchy | Hostile/unknown tracks render above friendly tracks in the z-order. The threat is always the top visual layer. Receiver positions are shown as distinct icons (antenna markers, blue) for spatial context without visual competition with emitter tracks. |
| Performance | UI updates are batched to 5 Hz. The pipeline never blocks on rendering. WebSocket push from FastAPI ensures sub-second latency from observation to screen. |
We chose to go deep on two hard problems rather than shallow across ten:
1. Semi-supervised hostile discovery. The autoencoder-plus-clustering pipeline is architecturally distinct from the supervised classifier. It learns a manifold of "normal" (friendly) signals and measures how far each new observation deviates. The DBSCAN layer then organizes the anomalies into coherent groups — discovering hostile signal types from unlabeled data. This is a real-world SIGINT workflow: you know your own forces' signatures; you discover the adversary's by observing what doesn't match.
2. Observation association under ambiguity. The simulation feed is a flat stream of independent detections. Before any geolocation can happen, we must solve the combinatorial problem of grouping observations into emitter events. Our three-criteria association engine (temporal proximity, classification similarity, RSSI geometric consistency) handles simultaneous emitters, missing receivers, and classification disagreements between receivers. This is the hidden hard problem that determines whether the map shows meaningful positions or noise.
Additional technical depth includes: calibrated confidence scores (temperature scaling), confidence gates on geolocation (degraded fixes are flagged, not hidden), CoT/ATAK XML export for interoperability with the actual military COP ecosystem, and a deadman switch for feed resilience (automatic fallback to replay mode if the live feed fails).
Our demo is designed around a narrative arc, not a feature list:
- The problem — three questions an operator must answer under pressure.
- The key insight — this is semi-supervised; we discover hostile signals, not just classify friendly ones.
- Live COP walkthrough — markers appearing in real time, color-coded, with uncertainty rings and staleness.
- The "aha" moment — hostile subtypes emerging on the map that were never in the training data.
- Operational credibility — CoT export button, demonstrating awareness of the ATAK ecosystem the CAF actually uses.
A pre-recorded video backup ensures the demo runs regardless of network conditions.
| Component | Technology | Rationale |
|---|---|---|
| ML / Pipeline | Python 3.10+, XGBoost, PyTorch, scikit-learn, scipy, numpy | Industry-standard ML stack. CPU-trainable on consumer hardware. |
| Anomaly Detection | Custom autoencoder (PyTorch), DBSCAN (scikit-learn), one-class SVM | Semi-supervised pipeline — learns friendly manifold, discovers hostile types. |
| Geolocation | scipy.optimize.least_squares, numpy (Kalman) | Nonlinear optimization for trilateration. Custom Kalman filter for moving targets. |
| Backend API | FastAPI + WebSocket | Native async, WebSocket support for real-time COP push, auto-docs. |
| COP Frontend | Mapbox GL JS v3.6.0 + OpenStreetMap | GPU-accelerated WebGL rendering, smooth animations, sufficient for <100 entities. |
| Export | Cursor on Target (CoT) XML | NATO-standard format, ingestible by ATAK/TAK — the COP the CAF uses. |
find-my-force/
├── shared/
│ ├── models.py # Typed dataclasses — the interface contract
│ └── config.py # Receiver positions, path-loss params, constants
├── ml/
│ ├── explore.ipynb # Signal exploration and visualization
│ ├── features.py # 25+ feature extraction from IQ vectors
│ ├── classifier.py # Supervised ensemble (XGBoost + CNN)
│ ├── anomaly.py # Autoencoder + one-class SVM OOD detection
│ ├── cluster.py # DBSCAN hostile subtype discovery
│ └── predict.py # Unified inference entry point
├── pipeline/
│ ├── associate.py # Three-criteria observation association
│ ├── trilaterate.py # Weighted RSSI trilateration + confidence gates
│ ├── tracker.py # Track state machine (create/update/age)
│ └── orchestrator.py # Main loop: feed → classify → associate → geo → tracks
├── feed/
│ ├── consumer.py # Live simulation feed client
│ ├── mock_feed.py # Synthetic feed for offline testing
│ └── replay.py # JSON-lines replay with deadman switch
├── api/
│ └── server.py # FastAPI + WebSocket bridge to COP
├── cop/
│ ├── index.html # Common Operating Picture dashboard
│ ├── app.js # Map logic, WebSocket client, UI state
│ └── styles.css # COP styling and visual hierarchy
├── export/
│ └── cot.py # Cursor on Target XML export
├── data/ # Training data, receiver config, path-loss params
├── tests/
│ └── test_pipeline.py # End-to-end integration tests
└── run_demo.sh # One-command boot: pipeline + API + COP
- Put downloaded RadarComm-style datasets in
data/RadarCommDataset-main/ordata/raw/. - Put generated training artifacts and transformed data in
data/training/ordata/processed/. - Keep only lightweight config files that should be versioned in
data/(for examplereceivers.json,path_loss.json). - Large datasets, archives, and model artifacts in these paths are intentionally ignored by
.gitignoreto keep the repo lean.
python3 -m venv .venvsource .venv/bin/activate.venv\Scripts\activatepip install -r requirements.txtFor competition mode, keep your key in a local file that is ignored by git:
New-Item -ItemType Directory -Force .secrets | Out-Null
Set-Content -Path .secrets/competition_api_key.txt -Value "<your-api-key>"The backend automatically reads this file via COMPETITION_API_KEY_FILE (default path:
.secrets/competition_api_key.txt). You can still override with env vars if needed.
./run_demo.shFEED_MODE=live FEED_URL=<simulation-endpoint> ./run_demo.sh| Service | URL |
|---|---|
| COP (map) | http://localhost:8080 |
| API docs | http://localhost:8000/docs |
| API root | http://localhost:8000 |
Both ports are configurable via environment variables
COP_PORTandBACKEND_PORT.
Stop the system --> Press Ctrl+C in the terminal running run_demo.shdeactivaterm -rf .venvThe H.A.R.P. mobile client is a React Native (Expo) app that mirrors the COP on iOS and Android — live track counts, hostile alerts, and an interactive satellite map with real-time blips.
- Node.js 18+
- Expo Go installed on your phone (App Store / Google Play)
- Phone and laptop on the same Wi-Fi network
cd mobile
npm installFind your laptop's local IP:
# macOS
ipconfig getifaddr en0
# Linux
hostname -I | awk '{print $1}'
# Windows (PowerShell)
(Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias Wi-Fi).IPAddressEdit mobile/src/config/env.ts and replace the LAN_IP value:
const LAN_IP = "<your-lan-ip>";In a separate terminal (from the project root):
./run_demo.shcd mobile
npx expo start --clearScan the QR code shown in the terminal with your phone's camera (iOS) or the Expo Go app (Android).
| Indicator | Meaning |
|---|---|
| LIVE (green badge) | Connected — tracks streaming |
| RECONNECTING (orange) | Can't reach backend — check that the LAN IP is correct and both devices are on the same network |
| OFFLINE (red) | WebSocket closed — restart the backend |
Tip: If the badge stays on RECONNECTING, open
http://<your-lan-ip>:8000/docsin Safari on your phone. If it doesn't load, your network has client isolation enabled — try a mobile hotspot or usengrok http 8000to tunnel.
| Role | Responsibility |
|---|---|
| ML Lead (P1) | Classifier architecture, feature engineering, CNN, autoencoder |
| ML / Eval (P2) | Data exploration, preprocessing, confidence calibration, scoring iteration |
| Association + Geo (P3) | Observation grouping, trilateration, track management, Kalman filter |
| COP Lead (P4) | Dashboard UI, map rendering, visual hierarchy, uncertainty display |
| Integration (P5) | Feed consumer, API layer, pipeline orchestration, CoT export, demo prep |
We didn't build a classifier and bolt on a map. We built a signals intelligence pipeline that mirrors the real-world SIGINT workflow: detect, classify, associate, locate, track, and present. Every stage is typed, tested, and decoupled.
The semi-supervised anomaly detection pipeline is the architectural differentiator. Most teams will train a six-class classifier and score zero on hostile detection. We learn what friendly looks like and discover what hostile looks like — then we cluster the threats into distinguishable subtypes that appear as distinct entities on the operator's map.
The COP is built for the operator, not for the engineer. Color, shape, opacity, and ring size each encode a distinct dimension of information. An operator can glance at the map and know, in under five seconds, what's out there, whether it's friendly, and how much to trust the data.
The CoT export isn't a checkbox — it's a statement that we understand where this technology lives in the real CAF ecosystem. Find My Force doesn't replace ATAK. It feeds ATAK.
Built for the Find My Force Hackathon — March 7, 2026 — University of British Columbia