Axiomancer
Documentation — Methodology

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:

Rendering diagram…
Course alteration detection gate: every condition must pass for an event to fire. The 6-hour circular mean handles compass wraparound correctly, and the port-zone exclusion prevents legitimate maneuvering from flooding the feed.
CriterionThresholdRationale
Vessel is underwayspeed > 0.5 knotsStationary vessels (drifting, anchored) are excluded — their heading is noisy and not meaningful.
Prior heading buffer≥ 5 fixes in the last 6 hoursShort windows produce unreliable circular means.
Coverage duration≥ 30 minutes between oldest fix and current fixSuppresses false positives from rapid bursts of fixes.
Course deviation≥ 45° from the 6-hour circular meanThe signed shortest angular distance — wraps correctly across 0°/360°.
Distance from port> 10 km from any port_zones polygonPort maneuvering produces legitimate sharp turns we don't want flooding the feed.
Per-vessel dedupNo prior emit in the last 6 hours for the same IMOPrevents 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
  }
}
FieldDescription
entity_id7-digit IMO number of the vessel that altered course.
event_typeAlways course_alteration.
event_categoryAlways kinematic_anomaly — useful for grouping with future kinematic detectors.
started_atTimestamp of the AIS fix that triggered the detection.
magnitudeAbsolute course deviation in degrees (always positive).
metadata.delta_degSigned deviation in (-180, 180]. Negative is a port-side (left) turn, positive is starboard.
metadata.median_heading_degCircular mean of the prior 6-hour heading buffer, in [0, 360).
metadata.current_heading_degVessel's heading at the moment of detection.
metadata.fix_countNumber of fixes in the prior buffer used to compute the median.
metadata.coverage_minutesSpan 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 geography ST_DWithin check against the port_zones table, 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.

Was this page helpful?

On this page