Junctions and the transition matrix¶
A junction is any point where two or more legs share an endpoint. Today’s projects often have several – a Y where two routes converge, an X where two through-routes cross, a chain of straight legs sharing intermediate waypoints. How traffic splits at these junctions matters for collision risk: a 70%/30% split between two onward legs implies a very different collision pattern than a 50/50 one, and not every leg that touches a junction necessarily exchanges traffic with every other.
OMRAT models this with a per-junction transition matrix
where each row sums to 1.0. Rows are populated from one of three sources, in order of preference:
AIS-derived – when a database connection is configured, OMRAT walks AIS tracks across each leg pair near the junction and normalises the per-MMSI counts. This is the most accurate source.
Geometry-derived – when AIS is unavailable, a fallback heuristic scores out-legs by their deflection angle from the inbound bearing (
exp(-deflection / 30 deg)) and normalises. Straight continuations get the highest share, hairpins the lowest.User-edited – entered or tweaked via Settings > Junction transition matrix…, marked
source="user", and preserved across subsequent Update all distributions runs.
How a transition matrix changes the calculation¶
Two ship-collision sub-models become matrix-aware:
Bend collisions¶
Today, bend collision frequency on a leg is computed from a single
bend_angle field plus the leg’s total traffic. When a junction
exists at the leg’s end and has a populated matrix, the model instead
iterates over each (this_leg -> other_leg) matrix row, computing
the deflection angle between inbound and outbound bearings and
weighting traffic by the matrix share. Pure-continuation pairs
(no deflection) drop out, and split-flow junctions naturally produce
multiple bend contributions.
If a leg has no junction at its end, or the junction has no matrix, the legacy single-bend path is used (no behaviour change).
Crossing / merging collisions¶
For each leg pair sharing a junction, the contribution to crossing / merging frequency is multiplied by a junction-aware conflict factor:
Reasoning: when 100% of leg 1 continues onto leg 2 and 100% of leg 2 continues onto leg 1, the only encounter at the junction is a head-on/overtaking exchange that the per-leg models already capture – the crossing/merging adjustment is 0. Any divergence (some traffic going elsewhere) re-introduces a junction-level conflict, hence the multiplicative complement. Defaults to 1.0 when no matrix is set.
Validating routes before computing¶
Pressing Update all distributions runs a route-validation pass before it touches AIS or the junction matrices. Two checks fire:
Close waypoints – distinct leg endpoints within 5% of the shortest incident leg’s length. The dialog zooms the canvas to the candidate and offers to snap to point 1, point 2, or the midpoint. The snap rewrites every leg endpoint matching either source coordinate to the chosen target.
Crossing legs (X-intersections) – two legs that geometrically cross at an interior point (not at a shared endpoint). The dialog asks whether to split each leg in two at the crossing point, producing four sub-legs that share a real junction node. Sub-legs inherit the parent leg’s traffic block.
After the validation pass, the junction registry is rebuilt against the (possibly mutated) segment list, AIS update runs as before, and junction matrices are refreshed – AIS-derived where data are available, geometry-derived where not, and user-edited rows are left alone.
Editing a matrix manually¶
Open Settings > Junction transition matrix…. The combo box lists every junction in the project, labelled with its id, location, and the legs meeting there. The grid below has rows for inbound legs and columns for outbound legs; the diagonal is fixed (a leg cannot transition to itself), and each cell is a percentage spinbox.
Click Save row to commit your edits for the currently displayed
junction. Values are normalised to sum to 1.0 on save (so you don’t
have to type exact percentages), and the junction’s source is
flipped to "user" so future Update all distributions runs leave
it alone.
JSON layout¶
The junctions block of a saved .omrat file is keyed by
junction id (a stable string derived from the coordinate, e.g.
"j_15.000000_55.000000"):
{
"junctions": {
"j_15.000000_55.000000": {
"point": [15.0, 55.0],
"legs": {"1": "end", "2": "start", "3": "start"},
"transitions": {
"1": {"2": 0.7, "3": 0.3},
"2": {"1": 1.0},
"3": {"1": 1.0}
},
"source": "user"
}
}
}
Behind the scenes¶
Pure-geometry route validation:
geometries/route_validation.py(close-waypoint detection, X-intersection detection, snap, split).Junction registry + matrix derivation:
geometries/junctions.py.Plugin handler (live registry + load/save):
omrat_utils/handle_junctions.py.Validation-pass UI driver:
omrat_utils/route_validation_ui.py.Matrix editor dialog:
omrat_utils/junction_matrix_dialog.py.AIS-derived counts:
compute/junction_transitions.py(pure-Python normaliser) plusomrat_utils/handle_ais.py:compute_junction_transitions(database query).