Course alteration anomalies
How Overwatch detects sudden vessel heading changes outside port zones — thresholds, circular-mean math, and the axiom_events course_alteration payload.
The AIS ingestion worker continuously evaluates each underway vessel's heading against its recent course history. When the current course deviates sharply from a vessel's circular-mean heading over the prior 6 hours — and the vessel is not maneuvering near a port — Overwatch emits a course_alteration row into the axiom_events table. These events surface unexpected mid-voyage turns that often correlate with rendezvous behavior, sanctions evasion, dark-fleet activity, or operational disruption.
When to use this
- Investigate vessels making unexplained turns away from their declared destination
- Build watchlist signals that flag mid-voyage rendezvous candidates
- Cross-reference course changes with STS encounters, dark events, or sanctions matches
- Power custom alert rules that route significant maneuvers to Slack, Teams, email, or webhooks
If you only need aggregate dark-fleet or risk-tier views, the Risk Intelligence endpoints are the right entry point. Use this event stream when you need granular, per-fix course-change signals.
Detection criteria
A course_alteration event is emitted only when all of the following conditions are met on a new AIS position fix:
| Criterion | Threshold | Rationale |
|---|---|---|
| Vessel is underway | speed > 0.5 knots | Stationary vessels (drifting, anchored) are excluded — their heading is noisy and not meaningful. |
| Prior heading buffer | ≥ 5 fixes in the last 6 hours | Short windows produce unreliable circular means. |
| Coverage duration | ≥ 30 minutes between oldest fix and current fix | Suppresses false positives from rapid bursts of fixes. |
| Course deviation | ≥ 45° from the 6-hour circular mean | The signed shortest angular distance — wraps correctly across 0°/360°. |
| Distance from port | > 10 km from any port_zones polygon | Port maneuvering produces legitimate sharp turns we don't want flooding the feed. |
| Per-vessel dedup | No prior emit in the last 6 hours for the same IMO | Prevents repeated alerts on a sustained turn. |
The 6-hour heading window uses a circular mean rather than an arithmetic mean so that compass wraparound is handled correctly. An arithmetic mean of [359°, 1°] yields 180° (wrong); the circular mean yields 0° (correct).
Event shape
Each detection writes a single row to axiom_events:
{
"entity_id": "9622629",
"entity_type": "vessel",
"event_type": "course_alteration",
"event_category": "kinematic_anomaly",
"started_at": "2026-04-28T14:32:11Z",
"magnitude": 87.4,
"confidence": 0.7,
"source": "aisstream-worker",
"product": "overwatch",
"metadata": {
"delta_deg": -87.4,
"median_heading_deg": 92.1,
"current_heading_deg": 4.7,
"current_speed_knots": 12.3,
"fix_count": 38,
"coverage_minutes": 312,
"latitude": 12.4,
"longitude": 43.8
}
}| Field | Description |
|---|---|
entity_id | 7-digit IMO number of the vessel that altered course. |
event_type | Always course_alteration. |
event_category | Always kinematic_anomaly — useful for grouping with future kinematic detectors. |
started_at | Timestamp of the AIS fix that triggered the detection. |
magnitude | Absolute course deviation in degrees (always positive). |
metadata.delta_deg | Signed deviation in (-180, 180]. Negative is a port-side (left) turn, positive is starboard. |
metadata.median_heading_deg | Circular mean of the prior 6-hour heading buffer, in [0, 360). |
metadata.current_heading_deg | Vessel's heading at the moment of detection. |
metadata.fix_count | Number of fixes in the prior buffer used to compute the median. |
metadata.coverage_minutes | Span between the oldest buffered fix and the current fix. |
Events are idempotent on (entity_id, started_at, event_type) — duplicate inserts are silently swallowed.
Querying
The axiom_events table is indexed for (event_type, started_at DESC) and (entity_id, event_type, started_at DESC).
-- All course-alteration events in the last 24 hours, largest turns first.
select
entity_id as imo_number,
started_at,
magnitude as turn_deg,
metadata->>'median_heading_deg' as prior_heading,
metadata->>'current_heading_deg' as new_heading,
metadata->>'latitude' as lat,
metadata->>'longitude' as lon
from axiom_events
where event_type = 'course_alteration'
and started_at >= now() - interval '24 hours'
order by magnitude desc;-- Course alterations for a watched vessel, joined with its current
-- risk profile.
select ae.started_at, ae.magnitude, ae.metadata, v.name, v.flag, v.risk_tier
from axiom_events ae
join vessels v on v.imo_number = ae.entity_id
where ae.event_type = 'course_alteration'
and ae.entity_id = $1
order by ae.started_at desc
limit 50;-- Course alterations clustered with STS or dark events on the same
-- vessel within a 24h window — a stronger signal than any one detector.
with course_events as (
select entity_id, started_at
from axiom_events
where event_type = 'course_alteration'
and started_at >= now() - interval '7 days'
)
select c.entity_id, c.started_at, count(distinct ae.event_type) as concurrent_signals
from course_events c
join axiom_events ae
on ae.entity_id = c.entity_id
and ae.started_at between c.started_at - interval '12 hours'
and c.started_at + interval '12 hours'
and ae.event_type in ('sts_encounter', 'dark_event', 'identity_change')
group by c.entity_id, c.started_at
having count(distinct ae.event_type) >= 1
order by concurrent_signals desc, c.started_at desc;Routing to alerts
Course alterations flow into the unified alert inbox automatically. To get notified for turns that match specific criteria — for example, large turns by watchlisted vessels, or any turn near a known STS hotspot — create an alert rule against the event_type field:
curl -X POST -H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
https://www.axiomoverwatch.io/api/v1/alerts/rules \
-d '{
"name": "Sharp turn on watched vessel",
"expression": {
"kind": "group",
"operator": "and",
"conditions": [
{ "kind": "condition", "field": "event_type", "op": "eq", "value": "course_alteration" },
{ "kind": "condition", "field": "magnitude", "op": "gte", "value": 90 }
]
},
"dedupe_window_minutes": 360
}'Tuning notes
The detector ships with conservative defaults that prioritize precision over recall:
- 45° threshold filters out minor course corrections common in transit traffic. Routine sea-keeping rarely produces sustained 45°+ deviations from a 6-hour mean.
- 6-hour window smooths over short-term zigzag patterns (weather avoidance, traffic separation schemes) while remaining responsive enough to catch genuine maneuvers.
- 10 km port buffer uses a GIST-indexed
geographyST_DWithincheck against theport_zonestable, so the per-fix overhead is negligible. - 6-hour per-IMO dedup window prevents a sustained heading change from producing repeated events as the buffer slowly catches up.
Tuning is currently a code-level change; if you need different thresholds for a specific deployment, contact support.
Course alteration is purely kinematic — it does not cross-check the vessel's declared destination, AIS nav status, or weather routing data. Treat it as a high-precision flag for "look harder at this voyage", not a standalone classification of intent.
Temporal edge materialization
How Axiom Overwatch turns vessel identity history into temporal edges — the substrate for shell-hop, jurisdiction-shopping, and MMSI-spoofing detection.
API overview
Authentication, access tiers, rate limiting, and conventions for the Axiom Overwatch REST API — the programmatic interface to maritime risk and trade data.