Axiom Overwatch derives vessel-to-vessel encounters from raw AIS positions and emits per-pair, per-timestep geometry — closest point of approach (CPA), time to CPA (TCPA), range, closing speed, and bearing rate. These records are the foundation Overwatch uses for collision-risk surfacing, ship-to-ship rendezvous detection, and forensic incident reconstruction.Documentation Index
Fetch the complete documentation index at: https://docs.axiomancer.io/llms.txt
Use this file to discover all available pages before exploring further.
When to use it
Reach for encounter data when you need to answer questions about how two vessels behaved relative to each other, not how one vessel behaved at a point in time. Typical use cases:- Reviewing near-miss geometry around a port approach or a traffic separation scheme
- Reconstructing the timeline of a collision, allision, or grounding for a forensics report
- Triggering downstream risk scoring (COLREGS, kinematic causality)
- Filtering for candidate ship-to-ship transfer windows before deeper investigation
How encounters are detected
For every pair of vessels in a spatial/temporal window, Overwatch streams their AIS positions through a hysteresis trigger. An encounter opens when the geometry crosses the entry threshold and closes after the exit threshold has held for a sustained window.Stream-merge per pair
For each unordered vessel pair, positions from both vessels are merged in time order. Every input timestamp triggers re-evaluation against the most recent fix from the other vessel — there is no interpolation between AIS reports, so observed geometry is always grounded in real broadcasts.
Geometry per epoch
At each evaluation tick, Overwatch computes range (nautical miles), relative bearings from each vessel, course difference, closing speed (knots), TCPA (seconds), DCPA (nautical miles), bearing rate (degrees per minute), and the pass side from each vessel’s perspective.
Hysteresis trigger
A deterministic risk probability
risk_prob is derived from range, TCPA, and DCPA factors. The encounter opens when risk_prob rises above the entry threshold and the geometric gates (range, TCPA, DCPA) are simultaneously inside their limits. It closes only after risk_prob has stayed below the exit threshold for the configured hysteresis window.Default thresholds
| Parameter | Default | Meaning |
|---|---|---|
range_max_nm | 6.0 | Maximum range, in nautical miles, for the geometry gate |
tcpa_max_s | 1800 | Maximum TCPA, in seconds (30 minutes) |
dcpa_max_nm | 1.5 | Maximum projected DCPA, in nautical miles |
p_risk_enter | 0.35 | risk_prob required to open an encounter |
p_risk_exit | 0.20 | risk_prob required (sustained) to close one |
hysteresis_s | 300 | Seconds the exit condition must hold (5 minutes) |
pair_max_age_s | 600 | Discard pairs whose latest fixes are more than 10 minutes apart |
Output shape
Two records are produced per detected encounter. Encounter (one row per detected window):vessel_a_id,vessel_b_id— canonical IMO ordering,vessel_a_id < vessel_b_idstart_ts,end_ts,cpa_ts— encounter window and the timestamp of minimum rangemin_range_nm,min_dcpa_nm,min_tcpa_s— geometry minima across the windowrisk_window_start_ts— the first epoch whererisk_probcrossed the entry thresholdobserved_fraction— fraction of epochs grounded in direct AIS observation (always1.0today; reserved for future imputation)encounter_conf— mean per-epoch geometry confidencecontext_tag— environmental context (open_sea,channel,tss,approach,anchorage); populated by a downstream classifiermax_course_change_a_deg,max_course_change_b_deg— largest single-step COG delta observed for each vessel across the encounter, in degreesrule17_deviation_a,rule17_deviation_b— boolean flags fired when the corresponding vessel’s mean stand-on probability across the encounter clears a threshold AND its max course change is large enough to count as a meaningful maneuver (see Rule 17 deviation detection)rule17_handoff_ts,rule17_handoff_trigger— the timestamp at which Rule 17(a) “keep course and speed” authority transitioned into Rule 17(b)/(c) “may / must take avoiding action,” and which gate fired (giveway_inactionorextremis). Both areNULLwhen no handoff condition tripped (see Rule 17 handoff timestamp)
encounter_id + ts_utc):
range_nm,rel_bearing_a_deg,rel_bearing_b_deg,course_diff_degclosing_speed_kt,tcpa_s,dcpa_nm,bearing_rate_deg_minpass_side_a,pass_side_b—-1(port),0(ahead/astern),1(starboard)risk_prob— deterministic geometry-only risk in[0, 1]p_head_on,p_overtaking,p_crossing— COLREGS rule posteriors derived from epoch geometry; sum to1p_special_context— reserved for TSS / narrow-channel / RAM context; leftNULLuntil spatial context ingestion landsgive_way_prob_a,stand_on_prob_a,give_way_prob_b,stand_on_prob_b— per-vessel role posteriors conditioned on the rule posteriors
Configuring an extraction
The extractor is exposed from@axiom/core for use in workers and Edge Functions. Pass a pre-filtered slice of AIS positions for a tractable spatial/temporal window — the multi-vessel form is O(n²) in the number of unique IMOs.
extractEncountersForPair(vesselA, vesselB, posA, posB, config) — vesselA must sort lexicographically before vesselB.
Production pipeline
In production, the extractor runs as a scheduled Edge Function rather than ad-hoc against@axiom/core. The function reads recent AIS positions, generates candidate vessel pairs with H3 spatial indexing, runs the same algorithm above, and persists results. Output is the source of truth for the Risk API, the Investigations API, and any downstream COLREGS / kinematic-causality pipelines.
Hourly schedule
Theextract-encounters-hourly cron job invokes the Edge Function at minute :07 of every hour. Each run pulls the trailing 90 minutes of AIS positions — a 30-minute overlap with the prior run guarantees encounters that straddle the hour boundary are seen end-to-end.
Results are persisted with two upserts:
pairwise_encounterkeyed on(vessel_a_id, vessel_b_id, start_ts)encounter_epochkeyed on(encounter_id, ts_utc)
Spatial pair generation
Naively pairing every vessel with every other vessel isO(n²) in the active fleet, which is intractable at production fleet size. Instead, each AIS position is bucketed into its H3 resolution-6 parent cell (~12 km edge, comparable to the 6 NM range gate). Within each cell, candidate pairs are generated as {IMOs in cell} × {IMOs in cell ∪ 1-ring neighbour cells}. The 1-ring expansion catches pairs that straddle a cell boundary; a canonical pair-key dedupe stops the same pair from being scored twice when both vessels are in the same cell.
Each run is bounded by a hard cap of 50,000 candidate pairs. If a window exceeds this, the function aborts with a pair_explosion error rather than running away — a guard against bad input data or pathological clustering.
Direct invocation
You can call the Edge Function directly to re-process a specific window — for example, after correcting upstream AIS data or for a forensic re-pull. Passsince and until as ISO 8601 timestamps; both are required when overriding the default 90-minute trailing window.
ingestion_logs with these counts and the window, so production runs are traceable from observability tooling.
Backfilling history
For multi-day re-processing — onboarding a new region, replaying after an algorithm change, or filling a gap — use thebackfill-encounters.mjs driver. It chunks a since → until range into fixed-length windows and POSTs each one to the Edge Function in sequence:
SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in the environment (or --supabase-url / --service-key flags). Because the Edge Function upserts on stable keys, re-running a botched chunk in place is safe — there is no need to drop rows before retrying.
As a runtime estimate: a 7-day backfill at 60-minute windows is 168 chunks; with the default 2-second sleep between chunks plus per-chunk Edge Function elapsed time, the full run takes roughly 10–20 minutes depending on position density.
Rule and role posterior inference
Every encounter epoch is annotated with COLREGS-aligned rule posteriors (p_head_on, p_overtaking, p_crossing) and per-vessel role posteriors (give_way_prob_a, stand_on_prob_a, give_way_prob_b, stand_on_prob_b). The rule posteriors are normalized to sum to 1 and are derived from epoch geometry alone — no calibrated model, no priors from prior epochs, and no environmental context. This is enough to surface meaningful rule and role probabilities for screening, timeline reconstruction, and downstream COLREGS adjudication, and it lets consumers stop carrying NULL-handling branches on these columns.
Rule posteriors
The three rule posteriors are produced by a smooth-step decision rule over course difference and the relative bearing each vessel sees of the other:| Rule | Geometric gate | ||
|---|---|---|---|
| Head-on (Rule 14) | Reciprocal courses (course_diff_deg ≥ 175°) and both vessels see the other near dead ahead (` | rel_bearing_*_deg | ≤ 7°`) |
| Overtaking (Rule 13) | Near-parallel courses (course_diff_deg ≤ 67.5°) and one vessel sees the other forward of the beam while the other sees its counterpart more than 22.5° abaft the beam (` | rel_bearing_*_deg | > 112.5°`) |
| Crossing (Rule 15) | Residual: 1 − p_head_on − p_overtaking |
Δc ≥ 178°, |θ| ≤ 2°) drives p_head_on ≥ 0.9, a clean overtaking and a clean crossing each cross 0.85, and ambiguous geometry produces a soft mixture instead of a brittle vote.
Role posteriors
Role posteriors are conditioned on the rule posteriors:- Head-on (Rule 14): both vessels are required to alter course to starboard — neither has stand-on priority.
give_way_prob_*andstand_on_prob_*each contribute0.5of the head-on mass to both vessels. - Overtaking (Rule 13): the vessel that sees the other more forward (smaller
|rel_bearing|) is the overtaker and is assigned the give-way mass; the overtaken vessel takes the stand-on mass. - Crossing (Rule 15): the vessel with the other on its starboard side is give-way. Concretely,
pass_side_a = +1means B is on A’s starboard, so A is give-way;−1is the mirror;0(other vessel dead ahead/astern) splits the crossing mass 50/50.
What the posteriors do not capture
- No special-context axis yet.
p_special_context(TSS, narrow channel, RAM, tug-tow) requires TSS polygon ingestion and avessels.nav_statusjoin that are not yet wired in. The column shipsNULLand will populate without a schema change when that lands. - No prior smoothing. Each epoch is scored independently. A pair that flips between crossing and overtaking geometry near the threshold will see the posteriors flip with it; downstream consumers that need a single rule label per encounter should aggregate across the encounter window (e.g., the rule with the highest mean posterior over the risk window).
- No nav-status priors. A vessel
not_under_commandorrestricted_in_ability_to_manoeuvreis treated identically to one underway and free to manoeuvre.
Rule 17 deviation detection
Per COLREGS Rule 17(a)(i), the stand-on vessel shall keep her course and speed. Rule 17(a)(ii) and Rule 17(b) only authorise — and ultimately require — the stand-on vessel to manoeuvre when the give-way vessel is clearly failing to keep clear. A meaningful course alteration by a vessel with high stand-on probability is therefore unusual on its face: it typically indicates that the give-way side did not act in time and the stand-on side was forced to break stand-on to avert collision. The deviation itself is an evidence signal worth surfacing.When to use it
Reach for the Rule 17 deviation flags when you need to triage which encounters to read in detail rather than scan all of them. Typical use cases:- Filtering for encounters where stand-on action was forced — i.e. likely give-way non-compliance
- Producing analyst worklists that lead with the encounters most likely to warrant manual COLREGS adjudication
- Annotating forensic timelines so investigators see the stand-on side’s late maneuver as a first-class event, not a buried column on the epoch table
How the flags are computed
Eachpairwise_encounter row carries two new pairs of columns: max_course_change_{a,b}_deg and rule17_deviation_{a,b}.
max_course_change_*_deg— largest single-step course-over-ground delta observed for that vessel across the encounter epochs. Computed by walking the per-tick AIS COG snapshots stored on each epoch and taking the maximum wrapped delta between adjacent ticks.rule17_deviation_*—TRUEwhen both gates hold for that vessel,FALSEotherwise:max_course_change_*_deg ≥ 10°— the maneuver was meaningful (small AIS-jitter wobble doesn’t fire)mean(stand_on_prob_*)across the encounter epochs≥ 0.6— the vessel was the stand-on side often enough that staying on course was the legal expectation
FALSE there. A drifting stand-on vessel that wobbles 5° fires neither gate. The combination of “high mean stand-on probability AND a real course change” is what makes the boolean specifically a Rule 17 signal rather than a generic “vessel turned” indicator.
The thresholds are deliberately tuned conservatively for Phase 1 — false positives in either direction here propagate to analyst worklists, so the defaults err on the side of only firing when both conditions are clearly met. They’re exported from @axiom/core/processing/encounter-extraction as RULE17_MIN_COURSE_CHANGE_DEG and RULE17_MIN_MEAN_STAND_ON_PROB so analyst tooling can probe with non-defaults without forking the algorithm.
Querying for fired deviations
The columns are indexed for the common analyst query “show me encounters where a stand-on vessel was forced into action recently”:start_ts DESC with the rule17_deviation_a OR rule17_deviation_b predicate keeps the worklist query cheap even at production fleet size.
What the flags do not capture
Rule 17 deviation detection is the first phase of a broader compliance-evidence pipeline. The Phase 1 flags ship from pure geometry and AIS COG; richer signals are deliberately deferred to follow-up phases:- No counterfactual compliance distance. The flag answers “did the stand-on vessel maneuver?” not “how much did the give-way vessel’s path differ from a compliant one?”. Counterfactual compliance distance requires a maneuver-prediction model and is a separate ticket.
- No ghost-encounter inference. Encounters that would have happened if the stand-on vessel had not maneuvered (i.e. the near-miss the deviation prevented) are not back-projected onto the encounter table. That requires the same prediction model as the counterfactual distance and lands in the same follow-up.
- No nav-status priors. A vessel
not_under_commandorrestricted_in_ability_to_manoeuvreis treated identically to one underway and free to manoeuvre — the same caveat that applies to the rule and role posteriors above. - No special-context awareness. Encounters inside a TSS or narrow channel can carry rule-specific overrides that change what “stand-on” means; that requires the same TSS polygon ingestion that gates
p_special_contextand is not yet wired in.
Rule 17 handoff timestamp
Rule 17 deviation detection answers whether a stand-on vessel was forced into a meaningful maneuver across an encounter. The handoff timestamp answers when the legal authority for that maneuver activated — the moment Rule 17(a)‘s “keep course and speed” obligation transitioned into Rule 17(b)/(c)‘s “may / must take avoiding action.” Surfacing T* separately from the deviation flag is the operational disambiguation between premature, unnecessary deviation (the stand-on vessel broke course before authority transferred) and required avoidance (the stand-on vessel was already authorised, or compelled, to act).When to use it
Reach for the handoff timestamp when the deviation flag alone is too coarse — for example, when you need to:- Distinguish a stand-on vessel that maneuvered before T* (premature deviation, potentially itself a Rule 17 violation) from one that maneuvered after (required avoidance under Rule 17(b)/(c))
- Anchor a forensic timeline on the legal authority transition rather than on the geometric CPA, which usually lags T* by several minutes
- Compare give-way inaction encounters against extremis encounters when prioritising analyst review — extremis means the geometry collapsed regardless of give-way behaviour and typically warrants the most urgent attention
How T* is computed
For each encounter, Overwatch first picks the canonical give-way side — whichever vessel has the higher meangive_way_prob across the encounter epochs — and then walks the epochs in chronological order. The first epoch that meets either trigger fires T*; the earliest match wins.
| Trigger | Gates |
|---|---|
extremis | dcpa_nm < 0.1 (≈200 m). Geometry has already collapsed; Rule 17(c) compels stand-on action regardless of give-way behaviour. Short-circuits even if the give-way vessel is currently maneuvering. |
giveway_inaction | risk_prob > 0.70 and dcpa_nm < 0.5 and the give-way vessel’s max single-step COG change over the trailing 120-second action window is below 5°. Rule 17(b) authority for the stand-on vessel activates because the give-way vessel is observably failing to keep clear. |
@axiom/core/processing/encounter-extraction as RULE17B_RISK_PROB_THRESHOLD, RULE17B_DCPA_THRESHOLD_NM, RULE17B_ACTION_WINDOW_S, RULE17B_GIVEWAY_ACTION_TOL_DEG, and RULE17B_EXTREMIS_DCPA_NM so analyst tooling can probe with non-default values without forking the algorithm.
When neither trigger fires across the encounter, both rule17_handoff_ts and rule17_handoff_trigger ship NULL.
Querying for fired handoffs
A partial index onrule17_handoff_ts DESC keeps the common analyst worklist query — “show me encounters where Rule 17(b)/(c) authority transferred recently” — cheap at production fleet size:
rule17_handoff_ts IS NOT NULLANDrule17_deviation_*true — the stand-on vessel maneuvered after authority transferred (required avoidance).rule17_handoff_ts IS NOT NULLAND both deviation flags false — authority transferred but neither vessel acted; the encounter likely closed on its own kinematics, or both vessels were dangerously passive.rule17_handoff_ts IS NULLANDrule17_deviation_*true — the stand-on vessel maneuvered before any handoff trigger fired (potential premature deviation).
What the timestamp does not capture
The Phase 1 implementation emits T* and the trigger only. Several richer signals are deliberately deferred:- No pre/post-handoff deviation magnitudes. Phase 1 doesn’t report how much each side maneuvered before vs. after T*; that requires a maneuver-evidence accumulator the current emit-once pipeline doesn’t carry, and lands in Phase 2.
- No handoff confidence score. The trigger is a hard boolean per epoch; there is no continuous score capturing how decisively each gate cleared. Also Phase 2.
- No VTS or chokepoint hotspot aggregation. Per-port and per-corridor handoff-rate rollups are a Phase 2 deliverable and are not yet exposed via the API.
- No nav-status priors. A give-way vessel
not_under_commandorrestricted_in_ability_to_manoeuvreis treated identically to one underway and free to manoeuvre — the same caveat that applies to the rule and role posteriors. - No special-context awareness. TSS and narrow-channel rule overrides aren’t yet wired in; the handoff detector uses the same geometry-only inputs as the rest of the pipeline.
What the extractor doesn’t do
- No interpolation — every epoch is grounded in a real AIS report. Long gaps between fixes show up as long inter-epoch intervals, not synthetic samples.
- No heading-only inference — vessels missing speed or course are skipped for that epoch.
- No environmental context — currents, traffic separation rules, and depth restrictions are not modeled in the geometry-only
risk_prob. - No collision adjudication —
risk_probis a screening signal for downstream review, not an authoritative collision-risk verdict.
Related references
- Vessels API — single-vessel position and identity data feeding the extractor
- Risk API — surfaces encounter-derived risk indicators
- Investigations API — case files that pull encounter timelines into a forensics view