Fix segment avg_speed: derive from distance/time; tighten speed bounds to reject false matches

This commit is contained in:
Davide Scaini
2026-05-14 17:09:41 +02:00
parent 8ff781661e
commit 862226305a
+18 -7
View File
@@ -25,10 +25,17 @@ CONFORMANCE_MAX_FRAC = 0.30 # max fraction of interior points allowed to deviat
# 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
'cycling': 2.0, # ~7.2 km/h — below any realistic cyclist even on brutal climbs
'running': 0.8, # ~2.9 km/h
}
_MIN_SPEED_DEFAULT = 0.2 # hiking / walking / unknown
_MIN_SPEED_DEFAULT = 0.3 # hiking / walking / unknown
# Maximum geometric speed per sport in m/s — rejects GPS glitch matches.
_MAX_SPEED_MS: dict[str, float] = {
'cycling': 30.0, # ~108 km/h
'running': 12.0, # ~43 km/h
}
_MAX_SPEED_DEFAULT = 20.0
# ── fast distance approximation ───────────────────────────────────────────────
@@ -207,7 +214,10 @@ def _extract_effort(
) -> SegmentEffort:
elapsed_s = track.times[j] - track.times[i]
started_at = (track.started_at + timedelta(seconds=track.times[i])).replace(microsecond=0)
avg_speed = _avg_nonnull(track.speeds, i, j)
# Always derive avg speed from segment distance / elapsed time. Device-recorded
# speed is unreliable across formats (m/s vs km/h in older FIT files) and
# averaging instantaneous GPS speed over a slice gives different results anyway.
avg_speed = (seg.distance_m / elapsed_s * 3.6) if elapsed_s > 0 else None
avg_hr_raw = _avg_nonnull(track.hrs, i, j)
avg_hr = int(round(avg_hr_raw)) if avg_hr_raw is not None else None
avg_pwr_raw = _avg_nonnull(track.powers, i, j)
@@ -259,12 +269,13 @@ 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).
# Reject implausibly slow or fast matches.
elapsed = track.times[end_idx] - track.times[start_idx]
if elapsed > 0:
geo_speed = seg.distance_m / elapsed
min_speed = _MIN_SPEED_MS.get(track.sport, _MIN_SPEED_DEFAULT)
if seg.distance_m / elapsed < min_speed:
max_speed = _MAX_SPEED_MS.get(track.sport, _MAX_SPEED_DEFAULT)
if geo_speed < min_speed or geo_speed > max_speed:
search_from = start_idx + 1
continue