Ship-Ship Collision Calculations

This chapter describes OMRAT’s calculations for ship-ship collision risk. Four types of collision encounters are modelled: head-on, overtaking, crossing, and bend collisions, all based on the equations in Hansen (2008).

Overview

Ship-ship collisions occur when two vessels occupy the same space at the same time. The IWRAP methodology models this by calculating the geometric number of collision candidates – the expected number of close encounters per year assuming no evasive action – and then multiplying by a causation factor to get the actual accident frequency.

Four collision types: head-on, overtaking, crossing, and bend

The general formula is:

\[F_{\text{collision}} = N_G \times P_C\]

Where:

  • \(N_G\) = geometric collision candidates per year

  • \(P_C\) = causation probability (IALA default values)

Pipeline orchestrator: compute/ship_collision_model.py:526run_ship_collision_model()

Head-On Collisions

Head-on collisions occur between vessels travelling in opposite directions on the same leg. This is the classical encounter type for narrow shipping lanes.

Geometry

Two streams of traffic travel in opposite directions. The collision probability depends on the lateral overlap of their position distributions.

Equations (Hansen Eq. 4.2–4.4)

The geometric number of collision candidates is:

\[N_G = \frac{Q_1}{V_1} \times \frac{Q_2}{V_2} \times V_{ij} \times P_G \times L_w \times \frac{1}{\text{sec/year}}\]

Where:

  • \(Q_1, Q_2\) = traffic volume in each direction (ships/year)

  • \(V_1, V_2\) = vessel speeds (m/s)

  • \(V_{ij} = V_1 + V_2\) = relative closing speed (m/s)

  • \(L_w\) = leg segment length (m)

  • \(P_G\) = geometric collision probability

The term \(Q/V\) converts from traffic frequency (ships/year) to traffic density (ships/m), accounting for the time ships spend on the leg.

The geometric collision probability is:

\[P_G = \Phi\!\left(\frac{\mu_{ij} + B_{ij}}{\sigma_{ij}}\right) - \Phi\!\left(\frac{\mu_{ij} - B_{ij}}{\sigma_{ij}}\right)\]

Where:

  • \(\mu_{ij} = \mu_1 + \mu_2\) – combined mean lateral distance (head-on: positions add because ships face opposite directions)

  • \(\sigma_{ij} = \sqrt{\sigma_1^2 + \sigma_2^2}\) – combined standard deviation

  • \(B_{ij} = (B_1 + B_2) / 2\) – average vessel breadth (collision width)

  • \(\Phi\) = standard normal CDF

compute/basic_equations.py:46get_head_on_collision_candidates()

Head-on collision candidate calculation (compute/basic_equations.py)
# Matching them lets us bypass scipy's frozen-distribution dispatch (~2 ms
# per call) and use C-level ``ndtr`` / a direct Weibull formula.
_NORM_FUNC_RE = re.compile(
    r"\.norm\(loc\s*=\s*(?P<loc>-?\d+(?:\.\d+)?)\s*,\s*scale\s*=\s*(?P<scale>-?\d+(?:\.\d+)?)\s*\)\s*\.cdf\(x\)"
)
_WEIBULL_FUNC_RE = re.compile(
    r"\.weibull_min\(c\s*=\s*(?P<c>-?\d+(?:\.\d+)?)\s*,\s*loc\s*=\s*(?P<loc>-?\d+(?:\.\d+)?)\s*,\s*scale\s*=\s*(?P<scale>-?\d+(?:\.\d+)?)\s*\)\s*\.cdf\(x\)"
)


def _repair_fn(data: dict, drift_speed: float):
    """Return a callable ``f(distance) -> P(not repaired)`` for *data*.

    Caches the compiled scalar distribution callable, so the hot per-edge /
    per-ship call sites don't re-parse strings or re-create distribution
    objects.  For the two dominant shapes (``norm.cdf(x)`` /
    ``weibull_min.cdf(x)``) we bypass scipy's frozen-distribution machinery
    and call ``scipy.special.ndtr`` or a direct Weibull formula -- scipy's
    generic ``.cdf`` has ~2 ms of Python-level dispatch per call, and
    ``get_not_repaired`` runs tens of thousands of times per cascade.
    """
    key: tuple
    if data.get('use_lognormal') == 1:
        key = ('lognorm', float(data['std']), float(data['loc']),
               float(data['scale']), float(drift_speed))
    else:
        key = ('func', str(data.get('func', '')), float(drift_speed))

    fn = _REPAIR_FN_CACHE.get(key)
    if fn is not None:
        return fn

    if data.get('use_lognormal') == 1:
        # lognorm.cdf(x; s, loc, scale) = ndtr(log((x-loc)/scale) / s) for x>loc
        s = float(data['std'])
        loc = float(data['loc'])
        scale = float(data['scale'])

        def fn(distance: float, _s=s, _loc=loc, _scale=scale, _spd=drift_speed) -> float:
            drift_time = distance / _spd / 3600.0
            t = drift_time - _loc
            if t <= 0 or _scale <= 0:
                return 1.0

Default causation factor

\[P_C = 4.9 \times 10^{-5} \quad \text{(Fujii et al. 1974)}\]

Overtaking Collisions

Overtaking collisions occur between vessels travelling in the same direction at different speeds. The faster vessel catches up to the slower one.

Geometry

Both vessels travel in the same direction. The collision probability depends on the speed difference (overtaking is only possible if \(V_{\text{fast}} > V_{\text{slow}}\)) and the lateral overlap of their distributions.

Equations (Hansen Eq. 4.5)

The formula is structurally identical to head-on, but with different relative speed and lateral offset:

\[V_{ij} = V_{\text{fast}} - V_{\text{slow}}\]
\[\mu_{ij} = \mu_{\text{fast}} - \mu_{\text{slow}}\]

Note

If \(V_{\text{fast}} \leq V_{\text{slow}}\), overtaking is impossible and the result is zero.

The geometric probability \(P_G\), combined standard deviation \(\sigma_{ij}\), and breadth \(B_{ij}\) are calculated identically to the head-on case.

compute/basic_equations.py:147get_overtaking_collision_candidates()

Default causation factor

\[P_C = 1.1 \times 10^{-4} \quad \text{(Fujii et al. 1974)}\]

Crossing Collisions

Crossing collisions occur at intersections where two legs cross at an angle \(\theta\). This is common at traffic separation scheme junctions, port approaches, and areas where multiple routes converge.

Geometry

Two traffic streams cross at angle \(\theta\). The collision zone is an area around the intersection point whose size depends on vessel dimensions and the crossing angle.

Equations (Hansen Eq. 4.6)

\[N_G = \frac{Q_1 \times Q_2 \times D_{ij}} {V_1 \times V_2 \times |\sin\theta| \times \text{sec/year}}\]

Where:

  • \(\theta\) = crossing angle (radians)

  • \(D_{ij}\) = collision diameter (metres)

  • \(V_1, V_2\) = vessel speeds on each leg

The relative speed uses the law of cosines:

\[V_{ij} = \sqrt{V_1^2 + V_2^2 - 2 V_1 V_2 \cos\theta}\]

The collision diameter accounts for the projected area of both vessels at the crossing angle:

\[D_{ij} = (L_1 + L_2) \times |\sin\theta| + (B_1 + B_2) \times |\cos\theta|\]

Where \(L_1, L_2\) are ship lengths and \(B_1, B_2\) are ship beams.

Note

When \(\theta \to 0\) or \(\theta \to \pi\) (parallel or anti-parallel courses), \(\sin\theta \to 0\) and the crossing formula is not applicable. Use head-on or overtaking formulas instead.

compute/basic_equations.py:238get_crossing_collision_candidates()

Crossing collision with law of cosines (compute/basic_equations.py)

def get_head_on_collision_candidates(
    Q1: float,           # Traffic in direction 1 (ships/year)
    Q2: float,           # Traffic in direction 2 (ships/year)
    V1: float,           # Speed direction 1 (m/s)
    V2: float,           # Speed direction 2 (m/s)
    mu1: float,          # Mean lateral position dir 1 (m)
    mu2: float,          # Mean lateral position dir 2 (m)
    sigma1: float,       # Std dev lateral position dir 1 (m)
    sigma2: float,       # Std dev lateral position dir 2 (m)
    B1: float,           # Beam of vessel type 1 (m)
    B2: float,           # Beam of vessel type 2 (m)
    L_w: float           # Leg segment length (m)
) -> float:
    """
    Calculate geometric number of head-on collision candidates.

    Uses Hansen Eq. 4.2-4.4:
    N_G = Q1 × Q2 × V_ij × P_G × L_w

    Where:
    - V_ij = V1 + V2 (relative closing speed for head-on)
    - P_G = Φ((μ_ij + B_ij)/σ_ij) - Φ((μ_ij - B_ij)/σ_ij)
    - μ_ij = μ1 + μ2 (mean lateral distance between vessels)
    - σ_ij = √(σ1² + σ2²) (combined standard deviation)
    - B_ij = (B1 + B2)/2 (average vessel breadth)

    Parameters
    ----------
    Q1 : float
        Traffic in direction 1 (ships/year)
    Q2 : float
        Traffic in direction 2 (ships/year)
    V1 : float
        Speed direction 1 (m/s)
    V2 : float
        Speed direction 2 (m/s)
    mu1 : float
        Mean lateral position dir 1 (m)
    mu2 : float
        Mean lateral position dir 2 (m)

Default causation factor

\[P_C = 1.3 \times 10^{-4} \quad \text{(Pedersen 1995)}\]

How OMRAT distributes traffic between legs at a junction

A common question from new users:

“At a crossing or merging point, how does OMRAT decide which fraction of leg A’s traffic continues to leg B vs leg C?”

Short answer: it doesn’t. OMRAT does not model a routing distribution at junctions. Each leg has its own independent traffic table, and crossing collisions are computed pairwise from \(Q_1 \times Q_2\) for every pair of legs that share a waypoint.

That means the user is responsible for keeping leg traffic counts consistent:

  • If 1000 ships/year transit a fork and the user expects 60 % to take leg B and 40 % to take leg C, the Frequency (ships/year) value on leg A must be 1000, on leg B 600, on leg C 400 — entered manually on the Traffic tab (or split by AIS in the user’s pre-processing).

  • If you populate traffic from the AIS database (pbUpdateAIS), OMRAT samples passages from the AIS table that intersect each leg’s passage line independently. A ship that AIS shows transiting both leg A and leg B contributes +1 to leg A’s count and +1 to leg B’s count — i.e. AIS already supplies a self-consistent distribution as a side-effect of how passages are counted.

The crossing-collision contribution between leg \(i\) and leg \(j\) is therefore:

\[N_{G, i\to j} = \frac{Q_i \cdot Q_j \cdot D_{ij}} {V_i V_j |\sin\theta_{ij}| \cdot \text{sec/year}}\]

with no extra weighting term for “the share of \(Q_i\) that continues toward leg \(j\)”. The same applies to merging collisions (treated as crossings with a small angle) and to bend collisions on a single leg (no neighbouring-leg contribution).

compute/ship_collision_model.py:_calc_crossing_collisions iterates every pair (leg_i, leg_j) and skips pairs that don’t share a waypoint or are nearly parallel (crossing_angle < 0.1 rad).

Bend Collisions

Bend collisions occur at waypoints where a route changes direction. A vessel that fails to make the turn continues on its original heading and may collide with vessels that did turn correctly.

Geometry

At a waypoint, the route changes direction by angle \(\theta\). Most vessels turn correctly, but a small fraction \(P_0\) (typically 1%) fail to turn and continue straight.

Equations

\[Q_{\text{no turn}} = Q \times P_0\]
\[Q_{\text{turn}} = Q \times (1 - P_0)\]

The bend collision is then modelled as a crossing collision between the non-turning traffic and the turning traffic:

\[N_{\text{bend}} = N_G^{\text{crossing}}(Q_{\text{no turn}}, Q_{\text{turn}}, \theta)\]

Where \(\theta\) is the bend angle (change in heading at the waypoint) and the crossing collision formula is applied with self-interaction (same ship type on both “legs”).

compute/basic_equations.py:331get_bend_collision_candidates()

Default causation factor

\[P_C = 1.3 \times 10^{-4} \quad \text{(Pedersen 1995)}\]

Default probability of not turning:

\[P_0 = 0.01\]

Calculation Workflow

The collision calculation for each leg proceeds as follows:

  1. Extract traffic data: For each segment, extract all ship types, frequencies, speeds, and dimensions in both directions.

  2. Pair ship types: For head-on and overtaking, pair every ship type \(i\) in direction 1 with every ship type \(j\) in direction 2 (or same direction for overtaking).

  3. Calculate per-pair N_G: Apply the appropriate formula based on encounter type, using the specific vessel speeds, beams, and lateral distributions.

  4. Sum over all pairs: The total geometric candidates is the sum over all ship type pairs.

  5. Apply causation factor: Multiply by \(P_C\) to get accident frequency.

  6. Display results: Results are shown in the UI as annual accident frequencies per collision type.

Summary of Equations

Type

Relative Speed

Lateral Offset

Key Parameter

Head-on

\(V_1 + V_2\)

\(\mu_1 + \mu_2\)

Opposite directions

Overtaking

\(V_f - V_s\)

\(\mu_f - \mu_s\)

Same direction, \(V_f > V_s\)

Crossing

\(\sqrt{V_1^2+V_2^2-2V_1V_2\cos\theta}\)

\(D_{ij}\)

Crossing angle \(\theta\)

Bend

(crossing formula)

(crossing formula)

\(P_0 = 0.01\)