Skip to content

Commit c09bfe1

Browse files
Raise alerts for declared activity during a beacon malfunction
1 parent 975e917 commit c09bfe1

3 files changed

Lines changed: 94 additions & 26 deletions

File tree

pipeline/src/flows/beacon_malfunction_activity_detection.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def extract_vessels_with_recent_ais() -> pd.DataFrame:
3939
def extract_beacon_malfunctions_with_declared_activity() -> pd.DataFrame:
4040
"""
4141
Extract current beacon malfunctions during which the vessel recently declared
42-
fishing activity (DEP, FAR).
42+
fishing activity (DEP, FAR), along with the vessel data required to build alerts.
4343
This also works on vessels with paper logbook, although with some delay.
4444
"""
4545
return extract(
@@ -120,6 +120,17 @@ def beacon_malfunction_activity_detection_flow():
120120
ais_active_reportings = extract_active_reportings.submit(
121121
AlertType.AIS_ACTIVITY_ON_VESSEL_NOT_EMITTING_VMS_ALERT.value
122122
)
123+
declared_activity_silenced_alerts = extract_silenced_alerts.submit(
124+
AlertType.DECLARED_FISHING_ACTIVITY_DURING_BEACON_MALFUNCTION.value,
125+
# Fishing activity data may be received several months after the event,
126+
# and we should be able to detect those cases - i.e. a vessel that hasn't
127+
# emitted for 6 months and for which we receive today a declaration of
128+
# activity that took place 3 months must trigger the alert.
129+
number_of_hours=24 * 365,
130+
)
131+
declared_activity_active_reportings = extract_active_reportings.submit(
132+
AlertType.DECLARED_FISHING_ACTIVITY_DURING_BEACON_MALFUNCTION.value
133+
)
123134

124135
# Tag is_at_port using port H3 referential, then keep only at-sea vessels
125136
vessels_with_recent_ais = tag_positions_at_port(vessels_with_recent_ais)
@@ -147,9 +158,22 @@ def beacon_malfunction_activity_detection_flow():
147158
threat="Mesures techniques et de conservation",
148159
threat_characterization="VMS - absence",
149160
)
150-
filtered_alerts = filter_alerts(
161+
filtered_ais_alerts = filter_alerts(
151162
ais_alerts, ais_silenced_alerts, ais_active_reportings
152163
)
164+
declared_activity_alerts = make_alerts(
165+
current_malfunctions_with_declared_activity,
166+
AlertType.DECLARED_FISHING_ACTIVITY_DURING_BEACON_MALFUNCTION.value,
167+
"Activité de pêche déclarée pendant une avarie VMS",
168+
natinf_code=27688,
169+
threat="Mesures techniques et de conservation",
170+
threat_characterization="VMS - absence",
171+
)
172+
filtered_declared_activity_alerts = filter_alerts(
173+
declared_activity_alerts,
174+
declared_activity_silenced_alerts,
175+
declared_activity_active_reportings,
176+
)
153177

154178
# Load
155179
update_beacon_malfunction_is_followed.map(
@@ -162,6 +186,12 @@ def beacon_malfunction_activity_detection_flow():
162186
)
163187
load_new_beacon_malfunctions(new_malfunctions)
164188
load_alerts(
165-
filtered_alerts,
189+
filtered_ais_alerts,
166190
alert_config_name=AlertType.AIS_ACTIVITY_ON_VESSEL_NOT_EMITTING_VMS_ALERT.value,
167191
)
192+
load_alerts(
193+
filtered_declared_activity_alerts,
194+
alert_config_name=(
195+
AlertType.DECLARED_FISHING_ACTIVITY_DURING_BEACON_MALFUNCTION.value
196+
),
197+
)
Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
1-
WITH current_malfunctions AS (
2-
SELECT
3-
id,
4-
internal_reference_number AS cfr,
5-
vessel_status_last_modification_date_utc,
6-
is_followed
7-
FROM beacon_malfunctions bm
8-
WHERE stage IN ('INITIAL_ENCOUNTER', 'AT_QUAY', 'FOLLOWING', 'TARGETING_VESSEL') AND internal_reference_number IS NOT NULL
9-
),
10-
11-
latest_activities AS (
1+
WITH latest_activities AS (
122
SELECT
133
cfr,
144
MAX(activity_datetime_utc) AS latest_activity_datetime_utc
155
FROM logbook_reports
166
WHERE
17-
operation_datetime_utc >= :utcnow - INTERVAL '24h'
7+
operation_datetime_utc >= :utcnow - INTERVAL '24 hours'
188
AND operation_type = 'DAT'
199
AND log_type IN ('DEP', 'FAR')
2010
GROUP BY cfr
2111
)
2212

23-
SELECT
24-
cm.id,
25-
is_followed
26-
FROM latest_activities la
27-
JOIN current_malfunctions cm
28-
ON la.cfr = cm.cfr
29-
WHERE latest_activity_datetime_utc > cm.vessel_status_last_modification_date_utc
13+
SELECT DISTINCT ON (bm.id)
14+
bm.id,
15+
bm.is_followed,
16+
bm.internal_reference_number AS cfr,
17+
COALESCE(v.external_immatriculation, lp.external_immatriculation) AS external_immatriculation,
18+
COALESCE(v.ircs, lp.ircs) AS ircs,
19+
COALESCE(v.vessel_name, lp.vessel_name) AS vessel_name,
20+
COALESCE(v.flag_state, lp.flag_state) AS flag_state,
21+
'INTERNAL_REFERENCE_NUMBER' AS vessel_identifier,
22+
v.id AS vessel_id,
23+
d.dml,
24+
COALESCE(f.facade::text, d.facade) AS facade,
25+
lp.risk_factor,
26+
la.latest_activity_datetime_utc AS triggering_behaviour_datetime_utc
27+
FROM beacon_malfunctions bm
28+
JOIN latest_activities la ON la.cfr = bm.internal_reference_number
29+
LEFT JOIN vessels v ON v.cfr = bm.internal_reference_number
30+
LEFT JOIN last_positions lp ON lp.cfr = bm.internal_reference_number
31+
LEFT JOIN districts d ON d.district_code = v.district_code
32+
-- The sea front is derived from the vessel's last known position, falling back to
33+
-- its district's sea front for vessels that never emitted (no last position). The
34+
-- beacon malfunction's own coordinates are the last VMS emission, not where the
35+
-- declared fishing activity took place, so they are not used here.
36+
LEFT JOIN facade_areas_subdivided f
37+
ON ST_Intersects(ST_SetSRID(ST_Point(lp.longitude, lp.latitude), 4326), f.geometry)
38+
WHERE
39+
bm.stage IN ('INITIAL_ENCOUNTER', 'AT_QUAY', 'FOLLOWING', 'TARGETING_VESSEL')
40+
AND bm.internal_reference_number IS NOT NULL
41+
AND la.latest_activity_datetime_utc > bm.vessel_status_last_modification_date_utc
42+
ORDER BY bm.id

pipeline/tests/test_flows/test_beacon_malfunction_activity_detection.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def reset_test_data_with_declared_activity(reset_test_data):
150150
Malfunction id=5 (CFR 'ABC000306959', stage INITIAL_ENCOUNTER,
151151
vessel_status_last_modification_date_utc 2h10 ago) is followed in the test data.
152152
Unfollow it and add a recent FAR declaration for that vessel so the flow
153-
re-follows it based on the declared fishing activity.
153+
re-follows it and raises an alert based on the declared fishing activity.
154154
"""
155155
e = create_engine(db="monitorfish_remote")
156156
with e.begin() as con:
@@ -186,20 +186,34 @@ def reset_test_data_with_declared_activity(reset_test_data):
186186
)
187187

188188

189+
DECLARED_ACTIVITY_ALERT = "DECLARED_FISHING_ACTIVITY_DURING_BEACON_MALFUNCTION"
190+
191+
189192
def test_flow_follows_malfunction_with_declared_activity(
190193
reset_test_data_with_declared_activity,
191194
):
192195
"""
193-
On top of the AIS path (malfunction id=6, see `test_flow`), the flow follows
194-
non-archived, not-yet-followed malfunctions during which the vessel recently
195-
declared fishing activity (malfunction id=5).
196+
On top of the AIS path (malfunction id=6, see `test_flow`), for every
197+
non-archived malfunction during which the vessel recently declared fishing
198+
activity (malfunction id=5), the flow:
199+
200+
- follows the malfunction if it is not already followed
201+
- raises a DECLARED_FISHING_ACTIVITY_DURING_BEACON_MALFUNCTION pending alert
202+
for the vessel
196203
"""
197204
headers = {
198205
"Accept": "application/json, text/plain",
199206
"Content-Type": "application/json;charset=UTF-8",
200207
"X-API-KEY": "backend_api_key",
201208
}
202209

210+
initial_pending_alerts = read_query(
211+
"SELECT * FROM pending_alerts", db="monitorfish_remote"
212+
)
213+
assert (
214+
DECLARED_ACTIVITY_ALERT not in initial_pending_alerts.alert_config_name.values
215+
)
216+
203217
with patch("src.shared_tasks.beacon_malfunctions.requests") as mock_requests:
204218
state = beacon_malfunction_activity_detection_flow(return_state=True)
205219

@@ -221,3 +235,14 @@ def test_flow_follows_malfunction_with_declared_activity(
221235
any_order=True,
222236
)
223237
assert mock_requests.patch.call_count == 2
238+
239+
final_pending_alerts = read_query(
240+
"SELECT * FROM pending_alerts", db="monitorfish_remote"
241+
)
242+
declared_activity_alerts = final_pending_alerts.loc[
243+
final_pending_alerts.alert_config_name == DECLARED_ACTIVITY_ALERT
244+
]
245+
assert declared_activity_alerts.internal_reference_number.tolist() == [
246+
"ABC000306959"
247+
]
248+
assert declared_activity_alerts.iloc[0].value == DECLARED_ACTIVITY_ALERT

0 commit comments

Comments
 (0)