snapshot of https://github.com/ActivityWatch/aw-core.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build / Test on ubuntu-latest, py-3.8 (push) Successful in 4m21s
Lint / lint (push) Successful in 25s
Build / Test on macOS-latest, py-3.8 (push) Has been cancelled
Build / Test on windows-latest, py-3.8 (push) Has been cancelled
CodeQL / Analyze (python) (push) Failing after 1s
Some checks failed
Build / Test on ubuntu-latest, py-3.8 (push) Successful in 4m21s
Lint / lint (push) Successful in 25s
Build / Test on macOS-latest, py-3.8 (push) Has been cancelled
Build / Test on windows-latest, py-3.8 (push) Has been cancelled
CodeQL / Analyze (python) (push) Failing after 1s
This commit is contained in:
121
aw_transform/flood.py
Normal file
121
aw_transform/flood.py
Normal file
@@ -0,0 +1,121 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from copy import deepcopy
|
||||
from typing import List
|
||||
|
||||
from aw_core.models import Event
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def flood(events: List[Event], pulsetime: float = 5) -> List[Event]:
|
||||
"""Fill short gaps between events and merge nearby equal-data events.
|
||||
|
||||
Events are ordered by timestamp and duration. Gaps no larger than
|
||||
``pulsetime`` are filled. Equal-data neighbours merge across the gap; when
|
||||
their data differs, both events extend to the midpoint. Splitting an
|
||||
uncertain interval evenly is independent of the surrounding event lengths
|
||||
and matches the Rust implementation.
|
||||
|
||||
Overlapping equal-data events merge. For overlapping differing-data events,
|
||||
final normalization gives the later event precedence so the result never
|
||||
double-counts time.
|
||||
|
||||
See https://github.com/ActivityWatch/activitywatch/issues/124 for the data
|
||||
collection uncertainty that flooding is intended to handle.
|
||||
"""
|
||||
# Originally written in aw-research: https://github.com/ActivityWatch/aw-analysis/blob/7da1f2cd8552f866f643501de633d74cdecab168/aw_analysis/flood.py
|
||||
# NOTE: This algorithm has a lot of smaller details that need to be
|
||||
# carefully considered by anyone wishing to edit it, see:
|
||||
# - https://github.com/ActivityWatch/aw-core/pull/73
|
||||
|
||||
events = deepcopy(events)
|
||||
# Sort by (timestamp, duration) so shorter same-timestamp events come first.
|
||||
# This matches aw-server-rust's sort_by_timestamp behavior and ensures
|
||||
# a well-defined processing order when events share the same timestamp.
|
||||
events = sorted(events, key=lambda e: (e.timestamp, e.duration))
|
||||
|
||||
# If negative gaps are smaller than this, prune them to become zero
|
||||
negative_gap_trim_thres = timedelta(seconds=0.1)
|
||||
|
||||
warned_about_negative_gap_safe = False
|
||||
warned_about_negative_gap_unsafe = False
|
||||
|
||||
for e1, e2 in zip(events[:-1], events[1:]):
|
||||
gap = e2.timestamp - (e1.timestamp + e1.duration)
|
||||
|
||||
if not gap:
|
||||
continue
|
||||
|
||||
# Sanity check in case events overlap
|
||||
if gap < timedelta(0) and e1.data == e2.data:
|
||||
# Events with negative gap but same data can safely be merged
|
||||
start = min(e1.timestamp, e2.timestamp)
|
||||
end = max(e1.timestamp + e1.duration, e2.timestamp + e2.duration)
|
||||
e1.timestamp, e1.duration = start, (end - start)
|
||||
e2.timestamp, e2.duration = end, timedelta(0)
|
||||
if not warned_about_negative_gap_safe:
|
||||
logger.warning(
|
||||
f"Gap was of negative duration but could be safely merged ({gap.total_seconds()}s). This message will only show once per batch."
|
||||
)
|
||||
warned_about_negative_gap_safe = True
|
||||
elif gap < -negative_gap_trim_thres and not warned_about_negative_gap_unsafe:
|
||||
# Events with negative gap but differing data cannot be merged safely
|
||||
logger.warning(
|
||||
f"Gap was of negative duration and could NOT be safely merged ({gap.total_seconds()}s). This warning will only show once per batch."
|
||||
)
|
||||
warned_about_negative_gap_unsafe = True
|
||||
# logger.warning("Event 1 (id {}): {} {}".format(e1.id, e1.timestamp, e1.duration))
|
||||
# logger.warning("Event 2 (id {}): {} {}".format(e2.id, e2.timestamp, e2.duration))
|
||||
elif -negative_gap_trim_thres < gap <= timedelta(seconds=pulsetime):
|
||||
e2_end = e2.timestamp + e2.duration
|
||||
|
||||
if e1.data == e2.data:
|
||||
# Preserve the longer neighbour's extent while merging across
|
||||
# the gap, matching the existing semantics for equal data.
|
||||
if e1.duration >= e2.duration:
|
||||
e1.duration = e2_end - e1.timestamp
|
||||
e2.timestamp = e2_end
|
||||
e2.duration = timedelta(0)
|
||||
else:
|
||||
e2.timestamp = e1.timestamp
|
||||
e2.duration = e2_end - e2.timestamp
|
||||
e1.duration = timedelta(0)
|
||||
else:
|
||||
# The gap is an interval of uncertainty: without evidence that
|
||||
# either neighbour owns more of it, split it at the midpoint.
|
||||
midpoint = e1.timestamp + e1.duration + gap / 2
|
||||
e1.duration = midpoint - e1.timestamp
|
||||
e2.timestamp = midpoint
|
||||
e2.duration = e2_end - midpoint
|
||||
|
||||
# Pairwise flooding can mutate an event after its previous pair has already
|
||||
# been processed. Normalize the final stream so downstream consumers never
|
||||
# double-count overlapping time. For differing data, the later event wins.
|
||||
normalized: List[Event] = []
|
||||
for event in (e for e in events if e.duration > timedelta(0)):
|
||||
while (
|
||||
normalized
|
||||
and normalized[-1].timestamp + normalized[-1].duration > event.timestamp
|
||||
):
|
||||
previous = normalized[-1]
|
||||
|
||||
if previous.data == event.data:
|
||||
end = max(
|
||||
previous.timestamp + previous.duration,
|
||||
event.timestamp + event.duration,
|
||||
)
|
||||
previous.duration = end - previous.timestamp
|
||||
break
|
||||
|
||||
previous.duration = event.timestamp - previous.timestamp
|
||||
if previous.duration <= timedelta(0):
|
||||
normalized.pop()
|
||||
continue
|
||||
|
||||
normalized.append(event)
|
||||
break
|
||||
else:
|
||||
normalized.append(event)
|
||||
|
||||
return normalized
|
||||
Reference in New Issue
Block a user