Code Flow: From “Run Model” to Results

This chapter is the implementation-side companion to the theory chapters (Drifting Risk Calculations, Ship-Ship Collision Calculations, Powered Grounding and Allision). Those chapters explain what OMRAT calculates – the formulas, the physical meaning of each term, and the inputs the analyst supplies. This chapter explains how those formulas are executed inside the code: which function the GUI invokes, what that function calls next, and how results flow back to the result line-edits on the main dialog.

The goal is that a developer can read this chapter alongside the source and confidently trace any line in a calculation result back to the code that produced it.

Reading map

This chapter gives the common frame (GUI button, background task, phase order, progress reporting). Each accident type’s detailed call tree lives in its own chapter:

Pair each of these with its theory counterpart:

Entry point: the “Run Model” button

The user triggers a run by clicking Run Model on the main plugin dialog. The wiring between that button and the background calculation is set up when the dialog is constructed:

When the button is clicked, omrat.OMRAT.run_calculation() runs on the Qt main thread:

  1. GatherData pulls every field from the UI – segments, traffic, depths, objects, drift params, causation factors, rose, repair distribution – into one plain Python dict named data. This dict is the sole input the rest of the pipeline consumes.

  2. A CalculationTask is constructed with the existing Calculation object and the data dict.

  3. Three Qt signals are wired:

    • progress_updated -> _on_calculation_progress() (logs lines to the QGIS message log),

    • calculation_finished -> _on_calculation_finished() (fans the results out to result-visualisation helpers),

    • calculation_failed -> _on_calculation_failed() (surfaces the exception to the user).

  4. The task is handed to QGIS’s task manager (QgsApplication.taskManager().addTask(task)), which starts its run() method in a background thread so the UI stays responsive.

Orchestrator: omrat.py:599run_calculation()

Background orchestration: CalculationTask

CalculationTask subclasses qgis.core.QgsTask so QGIS can schedule it and show progress in the task-manager tray. Everything inside its run() method executes on the background thread.

Class: compute/calculation_task.py:12CalculationTask

Progress plumbing

Before invoking the four risk models, run() installs a progress wrapper on the Calculation object:

def progress_wrapper(completed, total, message) -> bool:
    if self.isCanceled():
        return False                   # signal the calc to stop
    if total > 0:
        self.setProgress(int(completed / total * 100))
    self._update_description(message)  # shown in QGIS task tray
    self.progress_updated.emit(completed, total, message)
    return True                        # continue

self.calc.set_progress_callback(progress_wrapper)

Every risk model calls self._report_progress(phase, phase_progress, message) at key milestones (see compute/run_calculations.py:_report_progress). That helper converts (phase, 0.0..1.0) into an overall 0–100 percentage using fixed phase weights:

Progress phase weights inside a single risk model

Phase

Band

What it covers

spatial

0 – 40 %

Probability hole + min-distance pre-computation

shadow

40 – 60 %

Shadow polygons + per-obstacle edge geometry

cascade

60 – 90 %

Traffic cascade (per-ship lookups, cheap)

layers

90 – 100 %

Result-layer creation

Progress conversion: compute/run_calculations.py:80_report_progress()

The four phases, in order

CalculationTask.run() executes four phases sequentially on the same Calculation instance. Each phase writes its results into attributes of self.calc and also pushes formatted numbers straight into the result line-edits on the main dialog.

Phases invoked by CalculationTask.run()

#

Method on Calculation

Source

Outputs written

1

run_drifting_model()

compute/drifting_model.py:1608

drifting_allision_prob, drifting_grounding_prob, drifting_report, allision_result_layer, grounding_result_layer, LEPDriftAllision.setText, LEPDriftingGrounding.setText

2

run_ship_collision_model()

compute/ship_collision_model.py:526

ship_collision_prob, collision_report, LEPHeadOnCollision, LEPOvertakingCollision, LEPCrossingCollision, LEPMergingCollision

3

run_powered_grounding_model()

compute/powered_model.py:28

LEPPoweredGrounding, return: total frequency

4

run_powered_allision_model()

compute/powered_model.py:204

LEPPoweredAllision, return: total frequency

Between every phase, run() checks self.isCanceled() so the user can stop a long calculation. If a phase raises an exception, run() stores the message on self.error_msg and returns False; the finished() callback then emits calculation_failed on the main thread.

Where the inputs come from

Inside every phase, one dict (data) acts as the single source of truth. Its keys map onto specific UI widgets / files:

Top-level keys of the calculation data dict

Key

What it holds

segment_data

Per-leg geometry, direction labels, lateral distributions (mean/std/weight per direction), ai1/ai2 (IWRAP position check interval, seconds), bend angle, line length. Keys are leg IDs. Populated by the legs/segments tab.

traffic_data

Per-leg, per-direction matrices of frequency, speed, draught, beam, height. Rows = ship types; columns = LOA bins. Populated by the traffic tab or AIS import.

depths

List of [id, depth_m, wkt_polygon] triples for every depth contour. Populated by the depths tab.

objects

List of [id, height_m, wkt_polygon] triples for every structure (bridge pier, wind turbine foundation, …). Populated by the objects tab.

drift

Blackout rate drift_p, per-type blackout overrides blackout_by_ship_type, drift speed speed (knots), anchor probability anchor_p, anchor-depth factor anchor_d, repair distribution under repair (use_lognormal, std/loc/scale or func string), wind rose under rose.

pc

Causation factors per accident type (grounding, allision, headon, overtaking, crossing, bend, allision_drifting_rf, grounding_drifting_rf).

ship_categories

LOA bin definitions and ship-type names, used by the ship-collision model to estimate ship beam from LOA and to label the traffic matrix rows.

The GatherData helper reads each widget and builds this dict in one shot: gd.get_all_for_save(). The same dict format is what Storage serialises to .omrat files, so a loaded project can be handed to run_calculation without conversion.

UI -> dict: omrat_utils/gather_data.pyGatherData

The Calculation facade

Calculation is an empty class that pulls in five mixins at import time. Each mixin holds one clearly scoped piece of the pipeline:

class Calculation(
    DriftingModelMixin,
    ShipCollisionModelMixin,
    PoweredModelMixin,
    DriftingReportMixin,
    VisualizationMixin,
):
    """Main calculation facade -- composes all model mixins."""

Facade: compute/run_calculations.py:44Calculation

The mixins communicate only through attributes on self, which means each risk-model mixin can be tested in isolation with a mock parent. See tests/test_cascade_minimal.py for the smallest possible end-to-end drive.

Mixin source files

Mixin

Source

Covered in

DriftingModelMixin

compute/drifting_model.py

Code Flow: Drifting Model

ShipCollisionModelMixin

compute/ship_collision_model.py

Code Flow: Ship-Ship Collision Model

PoweredModelMixin

compute/powered_model.py

Code Flow: Powered Grounding & Allision (Cat II)

DriftingReportMixin

compute/drifting_report.py

Report markdown generation (written by the drifting model)

VisualizationMixin

compute/visualization.py

Map / plot helpers invoked after a run finishes

Completion and UI fan-out

When all four phases finish (or one fails), QGIS runs the task’s finished(result) method on the main thread. The main-thread handler emits whichever of these signals applies:

  • calculation_finished(self.calc) -> connected in omrat.py to _on_calculation_finished(), which:

    1. Calls _auto_save_run() to persist the finished run to the history GeoPackage (see “Run-history persistence” below).

    2. Calls refresh_previous_runs_table() so the TWPreviousRuns table on the Results tab picks up the new entry.

    3. (via VisualizationMixin) redraws the result layers, writes the drifting report Markdown file if a path is configured, and refreshes the overview panel.

  • calculation_failed(error_msg) -> connected to _on_calculation_failed(), which surfaces the error in the message log.

Because the line-edits on the main dialog were already set from inside the background phases, the user typically sees the numerical results before the finished signal fires.

Run-history persistence

_auto_save_run() delegates to omrat_utils.run_history.RunHistory, which writes a single GeoPackage at the user’s app-data location (see .omrat data format for the schema). The RunHistory class is intentionally QGIS-soft: every method except load_run_layers is plain sqlite3, so the persistence layer is fully unit-testable without a QGIS instance (tests/test_run_history.py covers save / list / get / compare / delete with 28 tests).

The table on the Results tab (TWPreviousRuns) is populated by refresh_previous_runs_table() and supports a right-click context menu with Load on map, Compare selected, and Delete. File -> Manage previous runs… is a shortcut to the same view.

Persistence: omrat_utils/run_history.pyRunHistory

Completion handler: omrat.py:635_on_calculation_finished()