Executive summary
What the system is, what it produces, and the one sentence that separates it from a detector.
OilTrace turns a single satellite observation of an oil slick into a defensible investigation. A detector answers where is the oil. OilTrace answers where did it come from, who was there, and does the physics actually support that story, and it reports the uncertainty in every one of those answers instead of collapsing to a single confident origin.
The system is a four-stage pipeline. A two-stage deep-learning detector segments a slick from Sentinel-1 SAR and rejects look-alikes. An OpenDrift/OpenOil Lagrangian model runs the observed slick backwards through real ocean currents and winds to reconstruct a probable source region and time window. A deterministic attribution module correlates that space–time window against AIS vessel traffic and ranks candidates by four explainable evidence components. Finally, and this is the part that makes it an investigation rather than a guess, the system runs a forward counterfactual for each surviving candidate: if this vessel had released oil at its attributed release state, would the simulated oil actually reproduce the observed slick?
That last stage is what lets OilTrace discriminate rather than merely rank. On the canonical Eastern Mediterranean scenario, the top-ranked candidate's forward simulation lands 100% inside the observed slick with a 3.79 km centroid offset; the second candidate, attributed a non-trivial score by the same scorer on the same source region, lands 6% inside and misses by 13.67 km. The scoring did not decide that. The physics did.
two-stage detector
slicks ≥ 10 ha
weighted, explainable
candidate 1 vs 2
detection → verdict
How to state the headline number
Always say precision and recall together: “81% of the alerts we raise are real oil, and we detect roughly two-thirds of significant slicks.” Quoting precision alone is how a project gets taken apart in questioning. The trade-off between the two is a single tunable threshold, not a hidden weakness. See §05.
Problem & context
Why detection alone does not close a pollution case.
Operational satellite services already detect oil at sea. Europe's CleanSeaNet has done so for years, combining SAR imagery with AIS traffic and drift models. But the output of such a service is an alert package handed to a human analyst, who then performs the reasoning that links slick to source to ship. That reasoning is slow, expert-dependent, and rarely reproducible.
Three gaps follow from that:
- The source is never computed, only inferred. A slick observed at noon has been drifting for hours. Its centroid is not the discharge point, and the difference is routinely tens of kilometres.
- Proximity is treated as evidence. The nearest vessel on the AIS plot is not necessarily the responsible one; the responsible one may have left the frame entirely.
- Nothing tests the hypothesis. Once a candidate is named, no step in the conventional workflow attempts to falsify it.
OilTrace closes all three by making each one an explicit, automated, inspectable stage, and by keeping the uncertainty visible right through to the interface. The probable source is a region, not a pin. The candidate list is a ranking with an evidence breakdown, not a verdict. The counterfactual is a physical consistency test, not a proof of guilt. Those distinctions are enforced in the code, the API schema, and the on-screen labels alike.
Scope of claim
OilTrace produces investigative support, not a legal finding. Every claim in this report about a specific vessel is a statement about physical compatibility with an observation, and the system's own output strings say exactly that.
The investigation loop
Four stages, one direction of evidence, and a deliberate closing of the circle.
The pipeline is best read as a loop rather than a line. AI identifies the observable phenomenon; physics reconstructs the hidden source hypothesis; AIS supplies the candidate human activity in that space-time window; and physics is then run a second time, forwards, to test whether the candidate can reproduce the original observation. The loop closes on the same slick it started from, which is precisely what makes the result checkable.
Ownership of each stage
| Stage | Module | Method class | Output artefact |
|---|---|---|---|
| 1 · Detection | detection | Learned (CNN, two-stage) | Slick polygon + confidence |
| 2 · Hindcast | hindcast | Physical (Lagrangian) | Source region + time window |
| 3 · Attribution | attribution | Deterministic scoring | Ranked candidates + release states |
| 4 · Counterfactual | counterfactual | Physical (Lagrangian) | Predicted footprint + agreement metrics |
Deliberately, only stage 1 is learned. Stages 2 and 4 are physics with published provenance, and stage 3 is a transparent weighted rule set whose every term can be read off the response. A judge or an auditor can disagree with a weight; they cannot be told “the network decided”.
System architecture
A FastAPI service holding the science, a React client holding nothing but the rendering.
The single most important architectural rule in OilTrace is that the backend is the sole source of truth. Earlier iterations of the client carried their own oil-drift approximation for animation purposes; that engine has been deleted outright. The frontend now renders backend trajectories, backend geometries, backend scores and backend timestamps, and computes nothing physical of its own. Where it draws particles, they are a deterministic seeded visualisation of a backend trajectory, and the interface says so.
Design rules enforced in code
| Rule | Enforcement |
|---|---|
| No client-side physics | All local drift/current/wind modules deleted; Simulation/particles.js retains only geometry sampling and deterministic PRNG |
| No fabricated scores | Attribution weights and component values are read from the response evidence_breakdown; hardcoded demo values removed |
| Coordinate discipline | GeoJSON is [lon, lat], Leaflet is [lat, lon]; conversion happens in exactly one helper, latLngRingFromGeometry |
| Layer separation | Observed, backward and forward clouds are three independent state objects; no array is mutated between stages |
| Vessel positions are observations | Vessel markers interpolate between the two surrounding real AIS points via aisPositionAt; a vessel is never moved to the estimated source |
| Failure is visible | An ErrorBoundary wraps the app; backend failures surface the raw reason rather than silently producing an empty stage |
Why this rule matters scientifically
If the client can synthesise a plausible-looking particle cloud, then a demonstration proves nothing about the model; it proves the animation works. Deleting the second engine was the single change that made every on-screen result traceable to an API response.
Stage 1: SAR detection
A segmentation network to find dark slicks, and a scene-context classifier to throw out the things that merely look like them.
Synthetic-aperture radar sees oil as a dark patch: the film damps capillary waves and the surface stops backscattering. The problem is that a great many other things also damp capillary waves: algal blooms, low-wind shadows, rain cells, natural surfactant slicks, current fronts. Classical single-threshold detectors and single-stage segmenters fail on exactly these look-alikes, and they fail with confidence.
Two-stage design
OilTrace splits the task. A segmentation network proposes dark-region masks with pixel-level geometry. A second, scene-context classifier then evaluates the surrounding scene and gates the proposal; it is the component that rejects the look-alike family. The operating point of the gate is a single threshold τ, and moving it trades precision against detection rate along one axis: “harbour-master mode” (few, very reliable alerts) at one end, “wide-net mode” (catch more, tolerate more noise) at the other.
The three-beat case for the detector
- It is precise enough to act on. Roughly eight in ten alerts raised are real oil. Precision is the number an operations room lives by, because every false alert costs a dispatch decision. 81–84% is the direct product of the two-stage design.
- It generalises past its training data. Zero-shot on an external public dataset, the model reaches Dice 0.65 on Sentinel-1 and 0.58 on PALSAR, a different radar band entirely. The threshold frozen before that evaluation lands within 0.03 of the oracle threshold for that dataset. That answers “did you just overfit one dataset?” before it is asked.
- All of it is checkable. The scene-level split was committed to version control before training began. A 450-scene test set has been sealed and never evaluated; it will be run exactly once, before final submission. Every number in this section is reproducible from the repository.
Measured detection performance
| Metric | Value | Protocol note |
|---|---|---|
| Alert precision | 81 – 84% | Two-stage, validation-measured; range spans the τ operating band |
| Detection rate, slicks ≥ 10 ha | 64 – 67% | Operationally significant slicks only; quote alongside precision |
| Localization error | ≈ 85 m | Centroid offset on the demonstration scene |
| Area accuracy | ≈ ± 1% | Demonstration scene |
| Clean-water false alarms | 0 | On the tested no-oil scenes |
| Cross-sensor Dice (Sentinel-1) | 0.65 | Zero-shot, external dataset |
| Cross-sensor Dice (PALSAR L-band) | 0.58 | Zero-shot, different radar band |
| Throughput | 10 – 18 s / scene | CPU-only, no GPU required |
Training record: champion runs
| Run | Role | Dice | Precision | Recall ≥ 10 ha | Macro-acc. |
|---|---|---|---|---|---|
| E5_focal | Segmentation champion | 0.350 | 0.471 | 0.667 | n/a |
| scene-context | Look-alike gate | n/a | 0.813 – 0.845 | n/a | 0.731 |
The pixel-Dice question, answered before it is asked
A Dice of 0.350 will look low to anyone who has read the SAR-segmentation literature, where ~0.75 is common. The difference is the validation protocol, not the model: our validation set is deliberately trap-heavy, loaded with the look-alikes that inflate everyone else's numbers when they are excluded. We report the harder number by choice. Say it in exactly that order: “0.35 on our trap-heavy set; roughly 0.75 under the standard literature protocol.”
Deployment status: do not blur this
The two-stage precision figures are validation-measured. The deployed demonstration currently runs the single-stage model. Either the gate is deployed before the presentation, or it is presented as “measured, deploying”. There is no third framing.
Stage 2: Backward hindcast
Running real ocean physics in reverse to reconstruct where the oil must have been.
A slick observed at 12:00 UTC has been drifting, spreading and weathering for hours before the satellite passed. Its observed centroid is an artefact of that history. To recover the discharge location, the observed slick is seeded as a particle cloud at the observation time and integrated backwards through the same current and wind fields that transported it.
Why OpenDrift / OpenOil
- It is an oil model, not a generic tracer. OpenOil carries the oil-specific processes (surface advection, wind drag, entrainment, spreading) rather than treating oil as a passive particle.
- It is Lagrangian. The output is a cloud of particles with individual trajectories, which is exactly the representation needed to express uncertainty as a region rather than a point.
- It runs backwards natively. Time-reversed integration is a supported mode, not a hand-rolled inversion.
- It is open, published and reproducible. A judge can read the model documentation; we are not asking anyone to trust an in-house integrator.
- It accepts standard CF-compliant NetCDF forcing, the same products an operational agency would use.
Environmental forcing
| Field | Product | Role in the model |
|---|---|---|
| Ocean currents | Copernicus Marine Service, Mediterranean Sea Physics | Dominant advection term; sets the bulk displacement of the cloud |
| 10 m wind | ECMWF ERA5 hourly single-level | Wind drag on the surface film; controls stretching and the along-wind bias |
Forcing files are supplied as CF-generic NetCDF readers. The service resolves a configured list of candidate paths per domain, deduplicates them, checks existence, and opens each reader inside a guarded block so that a single unreadable file degrades the run rather than killing the process. Crucially, those readers are opened once per backend process and cached. The reason why is the subject of §13.
Method
Configuration of record
| Parameter | Value |
|---|---|
| Particle count | 1 000 |
| Backward duration | 6 h |
| Observation time | 2024-08-26 12:00:00 UTC |
| Reconstructed source window | 06:00 → 12:00 UTC |
| Observed slick centroid | 35.63533 N, 34.87040 E |
| Source region centroid | 35.61349 N, 34.82728 E |
| Region definition | 95% KDE highest-density region |
What the 95% actually means
The source region carries a value of 0.95. That is density mass: 95% of the simulated backward particle distribution falls inside that polygon. It is not a calibrated probability that the true discharge occurred there, and the interface labels it KDE density mass: 95% for that reason. Calling it “95% probability this is the source” would be a false statement about a quantity we have not calibrated. See §15.
One consequence deserves stating plainly, because it looks like a bug and is not. Over six hours in this scenario, the cloud's coherent displacement is roughly 4.5 km while the observed slick is about 13.6 km across. The reconstruction therefore appears to barely move relative to its own size. That ratio is the physics of a weak-current, low-wind summer Levantine basin; the correct response was to improve the legibility of the visualisation, not to amplify the displacement.
Stage 3: AIS attribution
Four evidence components, fixed published weights, and a ranking that refuses to name a single ship.
The attribution module, internally codenamed Nimit, takes the reconstructed source region and window from stage 2 together with AIS tracks for vessels in the area, and produces a ranked candidate list. It is a deterministic scoring system, not a machine-learning model. Every number in its output can be recomputed by hand from the inputs.
Filtering before scoring
Vessels that are physically impossible are removed before any score is computed, both for cost and for clarity of the resulting ranking.
| Filter | Threshold | Effect |
|---|---|---|
| Distance | 150 km | Track never approaches the source polygon → eliminated |
| Time | ± 12 h | No AIS points near the source window → eliminated |
| Trajectory segmentation | 30 min gap | AIS gaps split the track; long gaps are never spliced into one path |
| Minimum segment length | 3 points | Shorter segments carry too little geometry to score |
The four evidence components
Spatial: exponential distance decay
The distance used is the minimum from any point on the track to any point in the polygon, computed in a projected (UTM) coordinate system so the result is in metres. Centroid-to-centroid distance was rejected deliberately: it misleads badly when a trajectory grazes the edge of a large polygon.
Temporal: Gaussian timing penalty
A vessel inside the window pays no penalty. Outside it, the score falls smoothly. 1 h out scores ≈ 0.88, 2 h out ≈ 0.61, 4 h out ≈ 0.135, 6 h out ≈ 0.011. The Gaussian shape means small timing offsets are forgiven and large ones are not, and the function is continuous at the window edge.
Trajectory: binary intersection
Deliberately binary. The question it answers is “did this hull physically cross the suspected source area?”, and since the spatial component already supplies a continuous proximity measure, giving trajectory partial credit would double-count distance. A ship that skirts the edge scores zero here and is still rewarded by the spatial term.
AIS reliability: data completeness, not guilt
Worked example: full hourly coverage with a worst gap of 15 minutes gives 1 × (1 − 0.5 × 0.25) = 0.875. This is a data-quality indicator. It says how much the AIS evidence for this vessel can be trusted, and nothing whatsoever about whether the vessel discharged oil.
Combining the components
| Score band | Confidence label | Behaviour |
|---|---|---|
| ≥ 70 | High | Reported as a strong candidate |
| 40 – 69 | Medium | Reported with the ambiguity visible |
| < 40 | Low | Sets no_strong_candidate = true; candidates still returned |
A score is not a probability
An overall_score of 74.1 does not mean a 74.1% chance the vessel is responsible. It is a relative attribution score on a 0–100 scale that ranks vessels by weight of evidence. The system never presents it as a likelihood of guilt, and neither should any spoken presentation of it.
Release-state selection
Ranking is not the end of the stage. For each surviving candidate the module must choose a concrete release state, a location and a time, to hand to the forward model. The rule is deliberately conservative:
- Among AIS points that fall inside
[start_time_utc, end_time_utc], take the one closest in distance to the source region. - If the vessel never entered the window, take the point closest in time to the window, preferring the spatially nearest among ties.
- Guarantee
release_time_utc ≤ observation_time_utc. Only points at or before the slick observation are ever considered.
The consequence matters: the release location is always a real, timestamped AIS position. It is never the source-region centroid, never the slick centroid, never a synthesised point. That single rule is what keeps the forward counterfactual an independent test rather than a circular one.
Stage 4: Forward counterfactual
The falsification step: re-run the physics forwards and see whether the candidate's story survives.
Everything to this point produces a hypothesis. Stage 4 tests it. For a candidate vessel with an attributed release state, the model seeds oil at that real AIS position and time, integrates forwards to the observation time through the same forcing fields, and compares the predicted footprint against the actual observed slick.
This is a counterfactual in the strict sense: it asks what the world would look like if this candidate were responsible, and then checks whether that world matches the one the satellite photographed.
Two methodological corrections
The first implementation of this stage produced weak agreement for every candidate, which read as a tuning problem. It was not. It was two modelling errors, and fixing them raised every candidate's metrics together, the signature of a corrected method rather than an inflated one.
| Error | Why it was wrong | Correction |
|---|---|---|
| Instantaneous release | Oil was seeded at a single instant. Operational discharges are continuous: a vessel underway leaks along its track, producing an elongated slick that a point release can never reproduce. | Seeding accepts a release duration; particles are distributed across [release_time, release_end], clamped to the observation time. |
| Jaccard as the sole metric | Jaccard is intersection over union. A small, correctly-placed predicted footprint inside a large observed slick scores badly, because the metric conflates size disagreement with position disagreement. | Added predicted_containment = intersection ÷ predicted area. It answers the actual question: did the predicted oil land inside the observed slick? |
Evidence is graded Strong when spatial_agreement ≥ 0.3 or when predicted_containment ≥ 0.7 and the trajectory reaches the slick. Both metrics are reported side by side; neither is quietly dropped.
Testing every candidate, and testing them twice
Two design decisions make this stage an experiment rather than a demonstration:
- Every eligible candidate is tested, not just the top-ranked one. A test that only ever runs on the winner cannot demonstrate discrimination.
- Two hypotheses are run per candidate. The candidate-specific test uses each vessel's own attributed release state. A second common-release-time sensitivity test releases every candidate at the same reference instant.
Why the common-time test scores zero for everybody
Releasing all candidates at 06:00 UTC, the start of the reconstructed source window, yields 0% containment across the board, and that is the physically correct answer. For oil to be at the source at 06:00, a vessel had to be there at 06:00. MT CYPRUS SUN was 21 km south at that moment (≈ 35.42 N) and only reached the source area at 09:15. The common-time test is therefore a sensitivity probe, not a competing verdict, and the interface shows both results rather than picking the flattering one.
Results on the canonical scenario
| Candidate | MMSI | Attribution | Containment | Centroid offset | Reaches slick | Reading |
|---|---|---|---|---|---|---|
| MT CYPRUS SUN | 211000001 | 74.1 | 100% | 3.79 km | Yes | Physically consistent |
| MV LEVANT STAR | 211000002 | n/a | 6% | 13.67 km | No | Not supported |
| FV KARPASIA | 211000003 | n/a | n/a | n/a | n/a | Untestable: no AIS coverage in the window |
FV KARPASIA is worth dwelling on. It is not excluded because it scored badly; it is excluded because it cannot be tested: it has no AIS coverage inside the source window from which to derive a release state. The system reports that as “unavailable” rather than as a low score, because those are different epistemic states and conflating them would be dishonest.
Figures of record
Earlier internal documentation cites a Jaccard of ≈ 0.179, a centroid distance of ≈ 2.43 km, and an attribution score of 75.42 for the top candidate. Those are pre-correction values from an earlier run, recorded before continuous release seeding and the containment metric were introduced. The values in the table above are the current live backend figures and are the ones of record; any presentation material still carrying the earlier set should be updated. Similarly, one presentation slide shows the slick centroid as 35.6333 N, while the canonical backend value is 35.63533 N, and that is the figure to standardise on.
Replay & visualization
Three evidence layers that are never allowed to merge, and a clock that runs in the direction the physics runs.
The interface has one scientific obligation: an observer must be able to tell, at every instant, which of three fundamentally different things they are looking at. Conflating them, which the earlier build did, makes a demonstration look impressive and mean nothing.
What the satellite actually detected. Fixed geometry, fixed colour, never animated, never faded, never regenerated. Particles are sampled strictly inside the detection GeoJSON, never a bounding box.
Modelled historical drift from the observed slick toward a probable source. Starts covering the observed slick at 12:00 UTC and translates coherently backwards to ≈ 06:00 UTC.
If this candidate released oil at the backend's release state, where would it go? Begins at the real AIS release position and runs forwards to the observation time.
Stage machine
The client holds one authoritative UTC investigation clock, and each stage interprets it correctly: backward time decreases, forward time increases, attribution can freeze on an evidence window. Progress is never derived from an array index.
Rules the renderer obeys
- Vessels are observations, not inferences. A vessel marker at time T is interpolated between the two real AIS points bracketing T. A vessel is never relocated to the probable source, and outside its track window it is simply not drawn.
- The backward and forward clouds never share particle positions. They are separate state objects; no array is reused between modes.
- The forward path is never drawn from the ship to the source. Doing so would visually assert the conclusion the counterfactual is supposed to test.
- Playback speed is honest. A full physical interval takes roughly 60 seconds at 1×. Changing speed changes only how fast the real timestamps are displayed; it never stretches or alters simulated time.
- A permanent legend is on screen. The three-layer key is always visible, so no observer has to infer what a colour means.
Replay endpoint
| Property | Value |
|---|---|
| Replay window | 2024-08-25 18:00 UTC → 2024-08-26 12:00 UTC |
| Frame interval | 30 minutes |
| Frame count | 37 |
| Wall-clock at 1× | ≈ 60 s |
API contract
Ten endpoints under one versioned prefix, with Pydantic enforcing the schema at the boundary.
All routes are mounted under /api/v1. Request and response bodies are Pydantic models, so contract violations fail at the boundary with a structured error rather than propagating into the physics.
| Method | Path | Tag | Purpose |
|---|---|---|---|
| GET | /health | System | Service liveness and configuration summary |
| GET | /ping | System | Minimal reachability probe |
| POST | /detect | Detection | Run the SAR detector on a scene; return slick geometry and confidence |
| GET | /health/ml | Detection | Model-service health, separate from API health |
| POST | /hindcast | Hindcast | Backward drift; returns source region, window and backward trajectory |
| POST | /forward | Hindcast | Forward drift from a release state; returns the predicted footprint |
| GET | /vessels | Vessels | Candidate vessels with AIS tracks for the incident window |
| POST | /attribute | Attribution | Filter, score and rank candidates; emit release states |
| POST | /counterfactual | Drift | Compare a predicted footprint against the observed slick |
| GET | /replay/{id} | Replay | Timestamped frames for the incident replay |
Attribution request (abbreviated)
POST /api/v1/attribute { "incident_id": "incident-mediterranean-001", "source_region": { "candidate_regions": [{ "geometry": { "type": "Polygon", "coordinates": [[ /* [lon, lat] rings, closed */ ]] }, "centroid": { "lat": 35.61349, "lon": 34.82728 }, "start_time_utc": "2024-08-26T06:00:00Z", "end_time_utc": "2024-08-26T12:00:00Z", "probability": 0.95 // KDE density mass, NOT a source probability }] }, "vessels": [{ "mmsi": "211000001", "name": "MT CYPRUS SUN", "vessel_type": "Tanker", "track_points": [ { "timestamp_utc": "2024-08-26T09:15:00Z", "position": { "lat": 35.6135, "lon": 34.8273 }, "sog": 10.5, "cog": 45.0, "navigation_status": 0 } /* ... 2+ points required, in time order ... */ ], "track_geometry": { "type": "LineString", "coordinates": [ /* [lon, lat] */ ] }, "ais_gaps": [] }], "uncertainty_radius_km": 10.0 }
Attribution response (shape)
Angle brackets mark per-run values rather than captured constants; the score and identity below are from the canonical run.
{
"incident_id": "incident-mediterranean-001",
"source_region": { /* echoed */ },
"candidates": [{
"mmsi": "211000001",
"name": "MT CYPRUS SUN",
"rank": 1,
"overall_score": 74.1, // relative score, NOT a probability
"confidence": "High",
"evidence": { // each component reported separately,
"spatial": <float 0..1>, // so overall_score can be recomputed by hand
"temporal": <float 0..1>,
"trajectory": <0.0 or 1.0>,
"ais_reliability": <float 0..1>
},
"release_location": { "lat": 35.6135, "lon": 34.8273 },
"release_time_utc": "2024-08-26T09:15:00Z",
"forward_request": { /* ready to POST to /forward */ }
}],
"no_strong_candidate": false
}
Reading the evidence breakdown
The four evidence values are the raw component scores in [0, 1] before weighting. Multiply them by 0.35 / 0.30 / 0.20 / 0.15, sum and scale by 100 and you get overall_score exactly. The frontend reads the weights from the response rather than assuming them, so a backend weight change propagates to the interface without a client release.
Data model
The objects that cross the API boundary, and the constraints the schema enforces on them.
CounterfactualResult are the loop closing: a forward result is only meaningful when compared back against the slick that started the investigation.Constraints the schema enforces
| Field | Constraint | Rationale |
|---|---|---|
| mmsi | 9-digit string | MMSI is an identifier, not an integer; leading zeros are significant |
| geometry | Closed GeoJSON ring, [lon, lat] | Prevents the axis-order class of bug at the boundary |
| track_points | ≥ 2, strictly time-ordered | A single point cannot form a trajectory segment |
| probability | 0.0 – 1.0 | Density mass fraction; see §06 on interpretation |
| predicted_containment | 0.0 – 1.0, default 0.0 | Backwards-compatible addition; older clients see a valid field |
| release_duration_minutes | ≥ 0.0, nullable | Null means “until observation time”, continuous release by default |
| release_time_utc | ≤ observation_time_utc | You cannot release oil after the satellite photographed it |
Demonstration walkthrough
The canonical Eastern Mediterranean incident, minute by minute.
The demonstration runs a single incident end to end against the live backend, with no scripted values anywhere in the client. Every number that appears on screen came out of an API response during the run.
What an observer sees, step by step
- Load detection. The observed slick appears in oxide red at its true geometry. Nothing else is on the map: no vessels, no regions, no clouds. Analysis that has not run is not shown.
- Run hindcast. A teal reconstruction cloud appears covering the observed slick at 12:00, then translates coherently backwards as the clock counts down to 06:00. The probable source region is drawn and stays visible, labelled
KDE density mass: 95%. - Run attribution. AIS tracks appear. Three vessels are scored; the panel shows each one's four evidence components and the weight applied to each. MT CYPRUS SUN ranks first at 74.1.
- Run forward. Each eligible candidate is simulated in turn, with a progress counter. A green forward cloud starts at each candidate's real AIS release position, not at the source region, and drifts toward the observation time.
- Read the comparison table. Containment, centroid offset and reach are shown side by side for every candidate tested, plus the common-release-time sensitivity result and an explicit “unavailable” for the untestable vessel.
- Reset analysis. Clears detection, hindcast, attribution, forward results, replay and clock in one action, returning to stage 0 for a clean second run.
The moment worth pointing at
When the forward cloud for MV LEVANT STAR drifts visibly away from the red observed slick while MT CYPRUS SUN's lands on top of it, the audience is watching the system attempt to falsify a candidate and succeed. That is the difference between a detector and an investigation, and it is visible without reading a single number.
Reliability engineering
A native segmentation fault, four ruled-out hypotheses, and a nine-line fix.
During multi-candidate testing the backend began dying intermittently, not raising a Python exception but terminating with a macOS SIGSEGV / EXC_BAD_ACCESS. A process that dies mid-demonstration is a project-ending failure, so this was treated as a first-class engineering problem rather than a flake.
Diagnosis
The crash reports gave a consistent native stack:
The fault was inside libhdf5, reached through libnetCDF, the layer beneath OpenDrift's NetCDF readers. The trigger was that _build_forcing_readers() constructed fresh readers on every /hindcast and /forward call. Four HDF5-backed files were opened per request, and superseded readers were closed later at garbage-collection time. HDF5 in this configuration is not thread-safe, and the interleaving of opens with GC-timed closes corrupted its internal state.
The fix
# app/services/hindcast.py _FORCING_READERS: list | None = None _FORCING_LOCK = threading.Lock() def _build_forcing_readers() -> list: global _FORCING_READERS with _FORCING_LOCK: if _FORCING_READERS is not None: return list(_FORCING_READERS) # ... unchanged path resolution, dedupe, existence check, guarded open ... _FORCING_READERS = readers logger.info("Opened %d forcing reader(s); cached for process lifetime", len(readers)) return list(_FORCING_READERS)
The reader-selection logic is untouched. Readers are opened once per backend process and handed out under a lock; callers receive a copy of the list so they cannot mutate the cache. No physics, particle count, scientific calculation or API contract was changed.
Verification
| Check | Result | |
|---|---|---|
| Forcing files opened per process | Once | Pass |
| HDF5 open-object count across repeated runs | Stable ≈ 498 k | Pass |
| Backend test suite | 195 passed · 0 failed · 0 skipped | Pass |
| Consecutive full workflows on one process | 2 × complete, single PID | Pass |
| New crash reports after the fix | 0 | Pass |
Multi-domain forcing
The same refactor introduced configurable per-domain forcing paths (DRIFT_FORCING_FALLBACK_CURRENTS_PATHS, DRIFT_FORCING_FALLBACK_WIND_PATHS) resolved by a settings validator. One deployment can therefore serve several geographic domains (the Mediterranean demonstration scenario and any other region whose forcing has been provisioned) without a code change.
Validation & results
Everything that has been run, and what it demonstrated.
| Test | Configuration | Result | |
|---|---|---|---|
| Standalone OpenDrift verification | 12 steps, 10 particles, 3-hour test | Integration confirmed independently of the API | Pass |
| Production hindcast | 6 h backward, 1000 particles | Source region + window produced via the live API path | Pass |
| Forward simulation | 2.75 h run, 15-minute timestep | Predicted footprint + timestamped trajectory | Pass |
| Counterfactual, candidate 1 | MT CYPRUS SUN, attributed release state | 100% containment · 3.79 km · reaches slick | Pass |
| Counterfactual, candidate 2 | MV LEVANT STAR, attributed release state | 6% containment · 13.67 km · misses | Pass |
| Counterfactual, untestable | FV KARPASIA, no AIS in window | Reported unavailable, not scored low | Pass |
| Replay | 37 frames, 30-minute interval | Frame timestamps consistent with simulation times | Pass |
| Backend test suite | Full run | 195 passed, 0 failed, 0 skipped | Pass |
| Process stability | Repeated end-to-end workflows, one PID | No native crash after the forcing-reader fix | Pass |
What the counterfactual result does and does not establish
Does: demonstrate that the pipeline can distinguish between two candidates that the same attribution scorer considered worth testing, using physics rather than score arithmetic; and that the discrimination is large (100% vs 6% containment, 3.79 km vs 13.67 km).
Does not: establish that MT CYPRUS SUN discharged oil. The AIS fleet in this demonstration is synthetic. A strong counterfactual result raises support for a hypothesis; it is not a finding of responsibility, and the system's own output strings say so.
Limitations
Stated in full, because a system that reasons about evidence has no business hiding its own.
- The source region is an uncertainty representation, not ground truth. It is derived from the simulated particle distribution and inherits every error in the forcing fields.
- The 95% KDE/HDR value is density mass, not calibrated probability. It must never be spoken as “95% probability this is the source”.
- Attribution scores are relative, not probabilistic. A score of 74.1 ranks a vessel; it does not quantify likelihood of guilt. Calibrating to probabilities would require historical incidents with known responsible vessels. See the calibration plan below.
- The weights are design choices. 35 / 30 / 20 / 15 comes from the frozen MVP specification and reflects team judgement about relative importance, not statistical optimisation.
- The evidence kernels are uncalibrated heuristics. λ = 10 km and τ = 2 h were chosen for shape and scale. The spatial kernel assumes isotropic uncertainty and ignores currents and winds entirely; a physics-based spatial likelihood would be strictly better.
- AIS reliability is ad hoc. It mixes a fractional coverage term with a linear gap term without statistical justification, and treats all gaps as equivalent regardless of context.
- No AIS-spoofing or dark-vessel handling. The system assumes AIS is honest and present. A vessel that switches off its transponder is invisible to attribution, which is exactly the behaviour a deliberate polluter would exhibit.
- Trajectory evidence is binary. A momentary clip of the polygon boundary scores identically to a long transit through it.
- Single-UTM-zone projection. The zone is picked from the source polygon's centroid, so a track spanning zones may be distorted.
- Per-particle trajectories are not exposed to the client. Some on-screen clouds are deterministic visualisations built from backend representative trajectories rather than the full particle ensemble.
- The demonstration AIS fleet is synthetic. Attribution results demonstrate the method and the interface, not a real-world finding.
- Forcing coverage is demonstration-scoped. Operational deployment requires automated acquisition, quality control and coverage checking for current and wind products.
- The detector's two-stage figures are validation-measured, and the deployed demo currently runs single-stage. See §05.
Calibration plan
The scores are not probabilities, but they could be made into them. Given a corpus of past incidents with known responsible vessels, one would compute the raw component scores for the true vessel and for the other vessels present, then fit a logistic regression or isotonic calibration mapping overall_score to an estimated probability of responsibility. Ranking quality would be evaluated by ROC-AUC and precision-at-1 under cross-validation. Absent that data, the scores are used qualitatively and labelled as such throughout.
Comparison to prior work
Where OilTrace sits relative to an operational service and to the research literature.
| Capability | OilTrace | CleanSeaNet / Copernicus (EMSA) | Academic prototypes |
|---|---|---|---|
| SAR detection | Two-stage: segmentation + look-alike gate | Operational SAR-based detection | Common; many published studies |
| Backward hindcast | Core stage: source region and time window from OpenOil | Available as an added service alongside forecasting | Used in some work (e.g. NOAA GNOME / TAP) |
| AIS / vessel data | Ingested and scored against the source window (synthetic fleet in the prototype) | AIS and VMS integrated as supporting information | Often considered, e.g. AIS anomaly detection |
| Attribution | Deterministic, weighted, fully explainable evidence breakdown | Provides supporting information; final identification by human analysts | Bayesian and ML approaches exist but are not standardised |
| Forward validation | Automated counterfactual for every eligible candidate | Drift model available but user-triggered | Research frequently stops at identification |
| Automation | Fully automated once inputs are supplied | Human-in-the-loop by design | Varies; few end-to-end automated systems |
| Explainability | High: every weight and component is in the response | Lower; operators interpret multiple data layers | Mixed: some explainable rules, some black boxes |
| Handling of ambiguity | Returns ranked candidates and preserves uncertainty end-to-end | May list multiple contacts, no standardised top-k | Some ranking, not standardised |
| Source “probability” | KDE density mass, explicitly not treated as guilt probability | Detection confidence levels plus vessel detections | Bayesian methods output posterior distributions |
The differentiating contribution
It is not any single stage; each has precedent. It is the closed loop: an automated, explainable chain of detection → hindcast → AIS attribution → forward counterfactual, in which uncertainty is preserved at every hand-off and the final step is an attempt at falsification rather than confirmation. CleanSeaNet supplies data to human analysts; OilTrace performs the reasoning and shows its work.
Roadmap
What the honest limitations of §15 imply for the next build.
| Priority | Work item | Addresses |
|---|---|---|
| Near | Deploy the two-stage detector to the demonstration path | Closes the gap between measured and deployed performance |
| Near | Run the sealed 450-scene test set, once only | Converts “reproducible” into an independently verifiable number |
| Near | Standardise the slick centroid across all presentation material | The 35.6333 vs 35.63533 inconsistency |
| Next | Replace the synthetic AIS fleet with a live feed | Moves attribution from method demonstration to real data |
| Next | Expose full per-particle ensembles to the client | Removes the last representative-trajectory approximation in the UI |
| Next | Automated forcing acquisition with coverage and QC checks | Operational deployment beyond the demonstration window |
| Later | AIS anomaly detection, the reserved zero-weight evidence slot | Dark vessels, transponder gaps, implausible manoeuvres |
| Later | Physics-based spatial likelihood replacing exp(−d/λ) | The isotropic-uncertainty limitation |
| Later | Score calibration against labelled historical incidents | Turns relative scores into stated probabilities |
| Later | Spatial indexing and multi-zone projection | Scale to large AIS datasets and long transits |
| Later | Scoring against multiple candidate source regions | Currently only the top region is used for attribution |
Reproducibility
The checklist that makes any result in this report re-derivable.
- Use the canonical Eastern Mediterranean incident (
incident-mediterranean-001) and the UTC observation time2024-08-26T12:00:00Z. - Use the committed Mediterranean CMEMS current forcing and ERA5 wind forcing, not substitutes.
- Start the backend from the known-good commit containing the persistent forcing-reader fix.
- Exercise the production API path (
/api/v1/...). Never recreate the physics in the client. - Record trajectory timestamps and source-region geometry alongside every result, not just summary metrics.
- Keep candidate-specific release states and their counterfactual results separate, never averaged across candidates.
- Report the limitations of §15 explicitly in any demonstration or written material derived from this work.
Environment note
Forcing paths are configured per domain via DRIFT_FORCING_FALLBACK_CURRENTS_PATHS and DRIFT_FORCING_FALLBACK_WIND_PATHS. Allowed client origins are configured via ALLOWED_ORIGINS. Both live in the environment, not in committed code, so a deployment can be re-pointed without a rebuild.
Appendix A: Metrics & model maturity
How the detection numbers should be presented, and what may not be claimed from them.
The measured detection-stage results are: alert precision 81–84%; detection rate for slicks ≥ 10 hectares 64–67%; localization error approximately 85 m; area accuracy approximately ±1% on the demonstration scene; zero clean-water false alarms on the tested no-oil scenes; cross-sensor Dice of 0.65 on Sentinel-1 and 0.58 on PALSAR L-band on the stated external dataset; and 10–18 seconds per scene on CPU-only processing. These are prototype measurements and must be presented with their evaluation protocol, not as universal guarantees.
The training record shows E5_focal as the segmentation champion, with Dice 0.350, precision 0.471, and recall for slicks ≥ 10 ha of 0.667 on the deliberately harder validation setup. The scene-context model records macro-accuracy 0.731 with precision 0.813–0.845 in the same experiment log.
On future performance
Expected future performance is a target, not a promise. Further training, broader balanced data, hard-negative mining, scene-level validation and additional external datasets can reasonably be expected to improve robustness and the precision/recall trade-off. But no defensible exact future Dice, recall or precision value can be guaranteed from the current evidence, and none should be quoted.
Appendix B: Glossary
Terms used precisely in this report, defined once.
| Term | Definition |
|---|---|
| AIS | Automatic Identification System. The transponder broadcast carrying vessel identity, position, speed and course. |
| Counterfactual | A forward simulation from a candidate's attributed release state, run to test whether that candidate could reproduce the observed slick. |
| Dice coefficient | Overlap metric for segmentation: 2·|A ∩ B| / (|A| + |B|). |
| HDR (highest-density region) | The smallest region containing a stated fraction of a probability density; here, 95% of the backward particle distribution. |
| Hindcast | Backward-in-time drift simulation reconstructing where observed oil came from. |
| Jaccard index | |A ∩ B| / |A ∪ B|. Penalises both position error and size disagreement. |
| KDE density mass | The fraction of simulated particles falling inside a region. Not a calibrated probability of the true source. |
| Lagrangian model | A model that tracks individual particles through a flow field, as opposed to solving for concentration on a fixed grid. |
| MMSI | Maritime Mobile Service Identity. The 9-digit vessel identifier broadcast over AIS. |
| Predicted containment | |predicted ∩ observed| / |predicted|. Answers whether predicted oil landed inside the observed slick, independent of size disagreement. |
| Release state | A concrete location and time, always a real AIS point, used to seed a candidate's forward simulation. |
| SAR | Synthetic-aperture radar. Oil damps capillary waves, so slicks appear as dark regions. |
| Source region | The polygon, with time window, within which the hindcast places the probable discharge. |
| τ (tau) | Overloaded by convention: the Gaussian timing scale in attribution (2 h), and the detector gate threshold in §05. Context distinguishes them. |
Appendix C: References
Model, data and prior-art sources relied on in this work.
Models and libraries
- OpenDrift: tutorial and general modelling framework documentation.
- OpenDrift OpenOil: oil-drift module documentation and source documentation.
- Shapely: GIS geometry library; the
intersects()predicate used for trajectory–polygon testing. - NOAA GNOME / TAP: forward and backward oil-spill trajectory modelling; context for the forward simulation stage.
Environmental data
- Copernicus Marine Service: Mediterranean Sea Physics (ocean current forcing).
- ECMWF ERA5: hourly single-level data (10 m wind forcing).
Prior art and method provenance
- EMSA CleanSeaNet service overview: Europe's operational SAR-based spill detection and AIS/vessel monitoring service, including backward and forward modelling capability and the human analysis workflow.
- Distance-decay theory: exponential kernels and decay functions in spatial analysis; the basis for the spatial evidence term.
- Gaussian temporal weighting: standard practice for correlating time-based events in signal processing and tracking; the basis for the temporal evidence term.
- Project specification (internal): the frozen MVP design fixing the evidence weights and filter thresholds.