Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespaces = false

[project]
name = "pipe-gaps"
version = "0.14.2"
version = "0.14.3"
description = "Tools for detecting interruptions in vessel position reporting systems (e.g., AIS, VMS)."
readme = "README.md"
license = "Apache-2.0"
Expand Down
24 changes: 23 additions & 1 deletion src/pipe_gaps/pipelines/raw_gaps/fns/process_boundaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,28 @@ def first_boundary(self):
def last_boundary(self):
return self._boundaries[-1]

def boundary_for_last_message(self):
"""Returns the boundary with the fullest lookback for the vessel's last message.

A message can fall inside two overlapping sliding windows at once, so more than
one boundary can share the same last message. The earlier-starting of those
windows reaches further back in time, so its boundary's ``end`` list is never
shorter -- and is often fuller -- than a later-starting window sharing the same
last message, whose own range begins later and leaves that earlier history out
entirely.

Returns the earliest-starting boundary (in ``self._boundaries``' own sort order)
whose own last message matches the overall last message across all boundaries --
unlike :meth:`last_boundary`, which simply picks whichever boundary starts latest,
regardless of whether it has the fullest history for that message.
"""
overall_last_key = timestamp_msgid_key()(self.last_message())

return next(
b for b in self._boundaries
if timestamp_msgid_key()(b.last_message()) == overall_last_key
)

def first_message(self):
return self.first_boundary().first_message()

Expand Down Expand Up @@ -147,7 +169,7 @@ def process(
# Step three:
# Create open gap if last message of last group met condition.
if self._eval_last:
last_boundary = boundaries.last_boundary()
last_boundary = boundaries.boundary_for_last_message()
last_message = last_boundary.last_message()

last_message_dt = datetime_from_timestamp(last_message[self.KEY_TIMESTAMP])
Expand Down
28 changes: 28 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,34 @@ class TestCases:
],
"id": "one_ssvid_with_open_gaps"
},
{
# The vessel's last message (the open gap's OFF message) falls in the 12h
# overlap shared by two consecutive sliding windows, both in range here
# (date_range spans 3 days so window B's own responsibility day, Jan 3,
# isn't filtered out by FilterWindowsByDateRange). The earlier satellite
# message, 4 hours before the OFF message, is only inside the
# earlier-starting window's own range -- the later-starting window's
# range begins after it, so its own boundary never includes it.
#
# eval_last must use whichever window has the fuller history for the
# vessel's last message, not simply whichever window starts latest.
"messages": [
create_message(time=datetime(2024, 1, 2, 9), receiver_type="satellite"),
create_message(time=datetime(2024, 1, 2, 13)), # open gap (last message).
],
"open_gaps": [],
"threshold": 6,
"date_range": ("2024-01-02", "2024-01-04"),
"expected_gaps": [
{
"positions_hours_before": 2,
"positions_hours_before_ter": 1,
"positions_hours_before_sat": 1,
"positions_hours_before_dyn": 0
},
],
"id": "one_ssvid_with_open_gap_at_window_overlap_boundary"
},
]

GAP_BETWEEN_ARBITRARY_PERIODS = [
Expand Down
Loading