# 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:526` – run_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. Values are read after `compute.data_preparation.apply_traffic_scaling()` has multiplied each cell by its `Scaling (%) / 100` – so the “easy option” global / per-row scaling on the Traffic tab is inherited transparently.
`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:526` – run_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.