From 3fc69b6dbb63822ddeaeec4c3209c19ea1304cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Link?= Date: Fri, 2 Oct 2026 16:09:29 -0300 Subject: [PATCH 1/2] Fix positions_hours_before undercounted for open gaps at window overlaps Boundaries.last_boundary() picked whichever boundary starts latest, not whichever has the fullest lookback for the vessel's last message. When that message falls in the overlap of two sliding windows, the later-starting window's own history is shorter, truncating positions_hours_before(_sat) for the resulting open gap. --- .../raw_gaps/fns/process_boundaries.py | 24 +++++++++++++++- tests/conftest.py | 28 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/pipe_gaps/pipelines/raw_gaps/fns/process_boundaries.py b/src/pipe_gaps/pipelines/raw_gaps/fns/process_boundaries.py index b94d48f6..e89fc2b8 100644 --- a/src/pipe_gaps/pipelines/raw_gaps/fns/process_boundaries.py +++ b/src/pipe_gaps/pipelines/raw_gaps/fns/process_boundaries.py @@ -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() @@ -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]) diff --git a/tests/conftest.py b/tests/conftest.py index 8d68f42d..58e9a887 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 = [ From 409934b38722bd261a6a940594d5d59d040862fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Link?= Date: Fri, 2 Oct 2026 16:36:53 -0300 Subject: [PATCH 2/2] Bump version to 0.14.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dfab6fb8..8c9cd654 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"