From 6e92ea4fceafe0707d56411ef0d4842584ac99d8 Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Wed, 13 May 2026 16:31:00 +0200 Subject: [PATCH] segments: reject false efforts via geometric speed check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long circuit rides were matching a segment START early and finding the segment END hours later on a second pass, producing effort times of ~17000s on a 4.7km segment. The conformance check passed because the full-circuit track covers all interior points within 50m over 5 hours. Add a per-sport minimum geometric speed (segment_distance / elapsed_s): cycling ≥ 1.0 m/s, running ≥ 0.5 m/s, default ≥ 0.2 m/s. When the check fails, advance past the current start candidate and retry, so a legitimate later match (e.g. a second lap done at real speed) is still detected. --- bincio/segments/detect.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/bincio/segments/detect.py b/bincio/segments/detect.py index af1626c..828d879 100644 --- a/bincio/segments/detect.py +++ b/bincio/segments/detect.py @@ -21,6 +21,15 @@ MATCH_RADIUS_M = 25 # max distance to segment start/end to open/close a CONFORMANCE_MAX_DEV_M = 50 # max allowed deviation for each interior segment point CONFORMANCE_MAX_FRAC = 0.30 # max fraction of interior points allowed to deviate +# Minimum geometric speed (segment_distance / elapsed_s) per sport, in m/s. +# Rejects false matches from long circuit rides where the track passes the +# segment start early and the segment end hours later. +_MIN_SPEED_MS: dict[str, float] = { + 'cycling': 1.0, # ~3.6 km/h — slowest realistic climb + 'running': 0.5, # ~1.8 km/h +} +_MIN_SPEED_DEFAULT = 0.2 # hiking / walking / unknown + # ── fast distance approximation ─────────────────────────────────────────────── _R = 6_371_000.0 # Earth radius in metres @@ -250,6 +259,15 @@ def detect_one(track: ActivityTrack, seg: Segment) -> list[SegmentEffort]: # No end found — no more efforts possible starting at or after start_idx. break + # Reject implausibly slow matches (e.g. circuit rides matching start + # early and end hours later on a second pass through the area). + elapsed = track.times[end_idx] - track.times[start_idx] + if elapsed > 0: + min_speed = _MIN_SPEED_MS.get(track.sport, _MIN_SPEED_DEFAULT) + if seg.distance_m / elapsed < min_speed: + search_from = start_idx + 1 + continue + if _conformance_ok(track, seg, start_idx, end_idx): efforts.append(_extract_effort(track, seg, start_idx, end_idx)) search_from = end_idx + 1