Code Flow: Ship-Ship Collision Model

This chapter walks the ship-ship collision model one function at a time, in the order the calls fire when a user presses Run Model. Pair it with Ship-Ship Collision Calculations, which derives the four collision formulas (head-on, overtaking, crossing, bend).

Entry point

ShipCollisionModelMixin.run_ship_collision_model() is phase 2 of CalculationTask (see Code Flow: From “Run Model” to Results). It takes the same data dict as the other phases and writes

  • self.ship_collision_prob – total frequency (accidents/year)

  • self.collision_report – per-leg + per-type breakdown

  • LEPHeadOnCollision / LEPOvertakingCollision / LEPCrossingCollision / LEPMergingCollision line-edit text

Entry: compute/ship_collision_model.py:526run_ship_collision_model()

Top-level call tree

run_ship_collision_model(data)
  |
  +-- for each leg:
  |     _calc_head_on_collisions(...)
  |       +-- _get_weighted_mu_sigma(seg_info, dir)
  |       |     +-- get_distribution(seg_info, dir)                  # compute/data_preparation.py
  |       +-- get_loa_midpoint / estimate_beam
  |       +-- get_head_on_collision_candidates(...)                  # compute/basic_equations.py
  |
  |     _calc_overtaking_collisions(...)
  |       +-- _get_weighted_mu_sigma(...)
  |       +-- get_overtaking_collision_candidates(...)
  |
  |     _calc_bend_collisions(...)
  |       +-- get_bend_collision_candidates(...)
  |
  +-- _calc_crossing_collisions(...)                                 # across leg pairs
  |     +-- _parse_point / _points_match / _calc_bearing
  |     +-- get_crossing_collision_candidates(...)
  |
  +-- aggregate into result dict
  +-- update LEP* line-edits

Unlike the drifting model, the ship-collision phase has no geometric precompute step. All heavy numerics live inside four pure-math functions in compute.basic_equations: get_head_on_collision_candidates(), get_overtaking_collision_candidates(), get_crossing_collision_candidates(), get_bend_collision_candidates(). Everything in the mixin is bookkeeping around those.

Data pulled from data

Field

Use

traffic_data[leg][dir]['Frequency (ships/year)']

Per-LOA, per-type frequency matrix. Same structure as the drifting model uses.

traffic_data[leg][dir]['Speed (knots)']

Per-cell speed. A cell can hold a scalar or a list; the code averages lists.

traffic_data[leg][dir]['Ship Beam (meters)']

Per-cell beam. Falls back to estimate_beam(LOA) when missing.

segment_data[leg] (mean1_1, std1_1, weight1_1 … per direction)

Lateral distribution parameters used by the head-on and overtaking formulas. _get_weighted_mu_sigma reduces them to a single \((\mu, \sigma)\) per direction.

segment_data[leg]['bend_angle']

Used only when it exceeds 5 degrees; drives the bend-collision formula.

segment_data[leg]['Start_Point'] / 'End_Point' / 'bearing'

Used by crossing-collision geometry to find waypoints and crossing angles between pairs of legs.

pc.headon / pc.overtaking / pc.crossing / pc.bend

Causation factors. The per-type geometric candidate count is multiplied by these to get the accident frequency.

ship_categories.length_intervals

Defines the LOA bins ([{'min','max'}]). get_loa_midpoint returns the centre of each bin for speed / beam estimation.

run_ship_collision_model(): orchestrator

Source: compute/ship_collision_model.py:526run_ship_collision_model()

Step by step:

  1. Short-circuit if traffic_data or segment_data is empty; set totals to zero and return.

  2. Read causation factors from data['pc'] (default to IALA recommended values).

  3. For every leg (outer loop is in this function), call three per-leg helpers:

    • _calc_head_on_collisions() - same-leg, opposite-direction.

    • _calc_overtaking_collisions() - same-leg, same-direction with different speeds.

    • _calc_bend_collisions() - same-leg geometric bend.

    Each helper returns a scalar frequency for that leg. Progress is reported at 80 % of the spatial phase because crossing collisions come next and still need the remaining 20 %.

  4. Call _calc_crossing_collisions() once for the full leg_keys list - it iterates every leg pair and looks for shared waypoints. Progress is reported inside the helper (cascade phase).

  5. Build the result report:

    self.collision_report = {
        'totals': result,   # head_on, overtaking, crossing, bend, total
        'by_leg': by_leg,   # per leg dict of head_on / overtaking / bend
        'causation_factors': {...},
    }
    
  6. Write the four result line-edits in one block, swallowing Exception because Qt widgets can disappear mid-run in pytest contexts.

Shared helpers

Three static helpers are used by every per-leg routine.

_get_weighted_mu_sigma()

Source: compute/ship_collision_model.py:58_get_weighted_mu_sigma()

Reads the per-direction lateral distributions (up to three superposed Gaussians) via compute.data_preparation.get_distribution(), normalises the weights to sum to 1, and reduces them to a single (mu, sigma) pair using the mixture-of-Gaussians variance identity:

\[\mathrm{Var}[X] = \sum_i w_i (\sigma_i^2 + \mu_i^2) - (\sum_i w_i \mu_i)^2\]

This is what the collision formulas expect as input. If the resulting \(\sigma\) is below 1 m the helper raises ValueError to catch misconfigured distributions early (the formula divides by sigma).

get_loa_midpoint(loa_idx, length_intervals)()

Returns the centre of LOA bin loa_idx. Used for beam estimation when the traffic cell doesn’t provide beam directly.

estimate_beam(loa)()

loa / 6.5 - a typical length-to-beam ratio. Only used as a fallback.

_calc_head_on_collisions()

Source: compute/ship_collision_model.py:138_calc_head_on_collisions()

Structure:

  1. Fetch direction 1 and direction 2 cells (leg_dirs[dir1] / leg_dirs[dir2]). If only one direction exists, head-on is zero.

  2. Extract per-cell freq, speed, beam arrays. For a cell that holds a scalar speed, that scalar is used; for a list/array, the mean is used.

  3. Get (mu1, sigma1) and (mu2, sigma2) via _get_weighted_mu_sigma().

  4. Double loop over every (loa_i, type_j) cell in direction 1 and every (loa_k, type_l) cell in direction 2. For each pair:

    n_g = get_head_on_collision_candidates(
        Q1=q1, Q2=q2, V1=v1_ms, V2=v2_ms,
        mu1=mu1_lat, mu2=mu2_lat,
        sigma1=sigma1_lat, sigma2=sigma2_lat,
        B1=b1, B2=b2, L_w=leg_length_m,
    )
    leg_head_on += n_g * pc_headon
    

The nested loop runs per leg; for a busy leg with 21 ship types x 5 LOA bins per direction there are ~10 k pairs, each a handful of arithmetic ops. No shapely.

The core formula is in compute.basic_equations.get_head_on_collision_candidates(). See Ship-Ship Collision Calculations for the math.

_calc_overtaking_collisions()

Source: compute/ship_collision_model.py:239_calc_overtaking_collisions()

Iterates each direction independently (overtaking is same-direction by definition). For each direction:

  1. Flatten every non-zero cell into a ship_cells list of (loa, type, freq, speed_ms, beam).

  2. Double loop over pairs (fast, slow) where v_fast > v_slow. The slower ship’s cell and the faster ship’s cell are passed to get_overtaking_collision_candidates() along with (mu_ot, sigma_ot) = the direction’s weighted lateral distribution.

  3. Multiply by pc.overtaking and accumulate into leg_overtaking.

_calc_bend_collisions()

Source: compute/ship_collision_model.py:313_calc_bend_collisions()

Bend collisions model a ship that fails to turn at the leg’s downstream waypoint and continues straight.

  1. Loop the leg’s direction cells to accumulate an average frequency (avg_freq) and average length / beam (avg_length, avg_beam). The averaging is a simple running mean weighted by non-zero cell count.

  2. Read segment_data[leg]['bend_angle']. If <= 5 degrees, return 0 (no meaningful bend).

  3. Otherwise, call get_bend_collision_candidates():

    n_g = get_bend_collision_candidates(
        Q=avg_freq, P_no_turn=0.01,
        L=avg_length, B=avg_beam,
        theta=bend_angle_rad,
    )
    

    P_no_turn is hard-coded at 0.01 (IALA default).

  4. Multiply by pc.bend and return.

_calc_crossing_collisions()

Source: compute/ship_collision_model.py:367_calc_crossing_collisions()

This is the only function that does cross-leg geometry.

  1. Outer double loop over every (leg1, leg2) pair with j > i to avoid double-counting.

  2. Parse each leg’s Start_Point and End_Point via _parse_point().

  3. Short-circuit unless the two legs share a waypoint (_points_match() comparing start/end pairs). This is the cheap filter that keeps the O(L^2) loop tractable.

  4. Compute each leg’s bearing from the stored bearing field or from _calc_bearing(start, end)().

  5. Crossing angle = abs(bearing1 - bearing2) reduced to [0, 90]. Angles below ~0.1 rad are treated as parallel and skipped.

  6. For every (dir1_cell, dir2_cell) pair on the two legs, call get_crossing_collision_candidates():

    n_g = get_crossing_collision_candidates(
        Q1=q1, Q2=q2, V1=v1_ms, V2=v2_ms,
        L1=l1, L2=l2, B1=b1, B2=b2,
        theta=crossing_angle_rad,
    )
    

    and multiply by pc.crossing before accumulating.

  7. Report progress (cascade phase) after every pair.

The pure-math helpers

All collision math lives in compute.basic_equations. Each helper is a short numpy expression with no external dependencies.

Source: compute/basic_equations.py

Function

Formula (see Ship-Ship Collision Calculations for derivation)

get_head_on_collision_candidates

\(N_G = \frac{Q_1}{V_1}\frac{Q_2}{V_2} V_{ij} P_G L_w / \text{s/yr}\), with \(P_G\) the Gaussian lateral-overlap probability.

get_overtaking_collision_candidates

Same structure but \(V_{ij} = |V_\mathrm{fast} - V_\mathrm{slow}|\) and only pairs with fast > slow contribute.

get_crossing_collision_candidates

\(N_G = \frac{Q_1 Q_2}{V_1 V_2} \frac{D}{\sin\theta} (L_1 + L_2 \sin\theta + B_1 + B_2 \cos\theta) / \text{s/yr}\).

get_bend_collision_candidates

\(N_G = Q \cdot P_\mathrm{no\_turn} \cdot (L + B \tan(\theta))\), simplified from Hansen for a single-leg bend.

sec/year = \(365.25 \times 24 \times 3600\).

Output

After _calc_crossing_collisions() returns, the orchestrator assembles:

result = {
    'head_on': total_head_on,
    'overtaking': total_overtaking,
    'crossing': total_crossing,
    'bend': total_bend,
    'total': sum of all four,
}
self.ship_collision_prob = result['total']
self.collision_report = {
    'totals': result,
    'by_leg': {leg: {'head_on': ..., 'overtaking': ..., 'bend': ...}, ...},
    'causation_factors': {...},
}

The four line-edits (LEPHeadOnCollision, LEPOvertakingCollision, LEPCrossingCollision, LEPMergingCollision) are set with f"{result[...]:.3e}". collision_report is the input to the ship-collision Markdown report written later by DriftingReportMixin (despite the name, the mixin writes all result reports).

Function reference

compute/ship_collision_model.py (all methods of ShipCollisionModelMixin)

  • run_ship_collision_model - orchestrator.

  • _calc_head_on_collisions - per leg.

  • _calc_overtaking_collisions - per leg, per direction.

  • _calc_bend_collisions - per leg, angle > 5 deg.

  • _calc_crossing_collisions - all leg pairs with shared waypoints.

  • _get_weighted_mu_sigma - lateral distribution reduction.

  • _parse_point / _points_match / _calc_bearing - geometry helpers.

  • get_loa_midpoint / estimate_beam - LOA/beam fallback estimates.

compute/basic_equations.py

  • get_head_on_collision_candidates

  • get_overtaking_collision_candidates

  • get_crossing_collision_candidates

  • get_bend_collision_candidates

compute/data_preparation.py

  • get_distribution - extract up to three superposed distributions from one segment’s mean/std/weight fields.